mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-11-18 03:34:12 +01:00
Merge branch 'develop' of https://github.com/NicolasNewman/mermaid into feature/2776_katex_math
This commit is contained in:
@@ -38,7 +38,7 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@braintree/sanitize-url": "^6.0.0",
|
||||
"@braintree/sanitize-url": "^6.0.1",
|
||||
"cytoscape": "^3.23.0",
|
||||
"cytoscape-cose-bilkent": "^4.1.0",
|
||||
"cytoscape-fcose": "^2.1.0",
|
||||
|
||||
@@ -1 +1 @@
|
||||
../mermaid/src/docs/syntax/zenuml.md
|
||||
../mermaid/src/docs/syntax/zenuml.md
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mermaid-js/mermaid-zenuml",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.2",
|
||||
"description": "MermaidJS plugin for ZenUML integration",
|
||||
"module": "dist/mermaid-zenuml.core.mjs",
|
||||
"types": "dist/detector.d.ts",
|
||||
@@ -33,7 +33,7 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@zenuml/core": "^3.0.0"
|
||||
"@zenuml/core": "^3.0.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"mermaid": "workspace:^"
|
||||
|
||||
22
packages/mermaid/.madgerc
Normal file
22
packages/mermaid/.madgerc
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"detectiveOptions": {
|
||||
"ts": {
|
||||
"skipTypeImports": true
|
||||
},
|
||||
"es6": {
|
||||
"skipTypeImports": true
|
||||
}
|
||||
},
|
||||
"fileExtensions": [
|
||||
"js",
|
||||
"ts"
|
||||
],
|
||||
"excludeRegExp": [
|
||||
"node_modules",
|
||||
"docs",
|
||||
"vitepress",
|
||||
"detector",
|
||||
"Detector"
|
||||
],
|
||||
"tsConfig": "./tsconfig.json"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mermaid",
|
||||
"version": "10.2.4",
|
||||
"version": "10.5.0",
|
||||
"description": "Markdown-ish syntax for generating flowcharts, sequence diagrams, class diagrams, gantt charts and git graphs.",
|
||||
"type": "module",
|
||||
"module": "./dist/mermaid.core.mjs",
|
||||
@@ -24,19 +24,23 @@
|
||||
],
|
||||
"scripts": {
|
||||
"clean": "rimraf dist",
|
||||
"dev": "pnpm -w dev",
|
||||
"docs:code": "typedoc src/defaultConfig.ts src/config.ts src/mermaidAPI.ts && prettier --write ./src/docs/config/setup",
|
||||
"docs:build": "rimraf ../../docs && pnpm docs:spellcheck && pnpm docs:code && ts-node-esm src/docs.mts",
|
||||
"docs:verify": "pnpm docs:spellcheck && pnpm docs:code && ts-node-esm src/docs.mts --verify",
|
||||
"docs:pre:vitepress": "pnpm --filter ./src/docs prefetch && rimraf src/vitepress && pnpm docs:code && ts-node-esm src/docs.mts --vitepress && pnpm --filter ./src/vitepress install --no-frozen-lockfile --ignore-scripts",
|
||||
"docs:build": "rimraf ../../docs && pnpm docs:spellcheck && pnpm docs:code && ts-node-esm scripts/docs.cli.mts",
|
||||
"docs:verify": "pnpm docs:spellcheck && pnpm docs:code && ts-node-esm scripts/docs.cli.mts --verify",
|
||||
"docs:pre:vitepress": "pnpm --filter ./src/docs prefetch && rimraf src/vitepress && pnpm docs:code && ts-node-esm scripts/docs.cli.mts --vitepress && pnpm --filter ./src/vitepress install --no-frozen-lockfile --ignore-scripts",
|
||||
"docs:build:vitepress": "pnpm docs:pre:vitepress && (cd src/vitepress && pnpm run build) && cpy --flat src/docs/landing/ ./src/vitepress/.vitepress/dist/landing",
|
||||
"docs:dev": "pnpm docs:pre:vitepress && concurrently \"pnpm --filter ./src/vitepress dev\" \"ts-node-esm src/docs.mts --watch --vitepress\"",
|
||||
"docs:dev:docker": "pnpm docs:pre:vitepress && concurrently \"pnpm --filter ./src/vitepress dev:docker\" \"ts-node-esm src/docs.mts --watch --vitepress\"",
|
||||
"docs:dev": "pnpm docs:pre:vitepress && concurrently \"pnpm --filter ./src/vitepress dev\" \"ts-node-esm scripts/docs.cli.mts --watch --vitepress\"",
|
||||
"docs:dev:docker": "pnpm docs:pre:vitepress && concurrently \"pnpm --filter ./src/vitepress dev:docker\" \"ts-node-esm scripts/docs.cli.mts --watch --vitepress\"",
|
||||
"docs:serve": "pnpm docs:build:vitepress && vitepress serve src/vitepress",
|
||||
"docs:spellcheck": "cspell --config ../../cSpell.json \"src/docs/**/*.md\"",
|
||||
"docs:release-version": "ts-node-esm scripts/update-release-version.mts",
|
||||
"docs:verify-version": "ts-node-esm scripts/update-release-version.mts --verify",
|
||||
"types:build-config": "ts-node-esm --transpileOnly scripts/create-types-from-json-schema.mts",
|
||||
"types:verify-config": "ts-node-esm scripts/create-types-from-json-schema.mts --verify",
|
||||
"checkCircle": "npx madge --circular ./src",
|
||||
"release": "pnpm build",
|
||||
"prepublishOnly": "cpy '../../README.*' ./ --cwd=. && pnpm -w run build"
|
||||
"prepublishOnly": "cpy '../../README.*' ./ --cwd=. && pnpm docs:release-version && pnpm -w run build"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -55,7 +59,7 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@braintree/sanitize-url": "^6.0.2",
|
||||
"@braintree/sanitize-url": "^6.0.1",
|
||||
"@types/d3-scale": "^4.0.3",
|
||||
"@types/d3-scale-chromatic": "^3.0.0",
|
||||
"cytoscape": "^3.23.0",
|
||||
@@ -65,7 +69,7 @@
|
||||
"d3-sankey": "^0.12.3",
|
||||
"dagre-d3-es": "7.0.10",
|
||||
"dayjs": "^1.11.7",
|
||||
"dompurify": "3.0.4",
|
||||
"dompurify": "^3.0.5",
|
||||
"elkjs": "^0.8.2",
|
||||
"katex": "^0.15.2",
|
||||
"khroma": "^2.0.0",
|
||||
@@ -82,7 +86,9 @@
|
||||
"@types/cytoscape": "^3.19.9",
|
||||
"@types/d3": "^7.4.0",
|
||||
"@types/d3-sankey": "^0.12.1",
|
||||
"@types/d3-scale": "^4.0.3",
|
||||
"@types/d3-selection": "^3.0.5",
|
||||
"@types/d3-shape": "^3.1.1",
|
||||
"@types/dompurify": "^3.0.2",
|
||||
"@types/jsdom": "^21.1.1",
|
||||
"@types/lodash-es": "^4.17.7",
|
||||
@@ -95,7 +101,6 @@
|
||||
"ajv": "^8.11.2",
|
||||
"chokidar": "^3.5.3",
|
||||
"concurrently": "^8.0.1",
|
||||
"coveralls": "^3.1.1",
|
||||
"cpy-cli": "^4.2.0",
|
||||
"cspell": "^6.31.1",
|
||||
"csstree-validator": "^3.0.0",
|
||||
@@ -112,7 +117,8 @@
|
||||
"remark-gfm": "^3.0.1",
|
||||
"rimraf": "^5.0.0",
|
||||
"start-server-and-test": "^2.0.0",
|
||||
"typedoc": "^0.24.5",
|
||||
"type-fest": "^4.1.0",
|
||||
"typedoc": "^0.25.0",
|
||||
"typedoc-plugin-markdown": "^3.15.2",
|
||||
"typescript": "^5.0.4",
|
||||
"unist-util-flatmap": "^1.0.0",
|
||||
|
||||
@@ -18,6 +18,7 @@ import { promisify } from 'node:util';
|
||||
|
||||
import { load, JSON_SCHEMA } from 'js-yaml';
|
||||
import { compile, type JSONSchema } from 'json-schema-to-typescript';
|
||||
import prettier from 'prettier';
|
||||
|
||||
import _Ajv2019, { type JSONSchemaType } from 'ajv/dist/2019.js';
|
||||
|
||||
@@ -46,6 +47,7 @@ const MERMAID_CONFIG_DIAGRAM_KEYS = [
|
||||
'er',
|
||||
'pie',
|
||||
'quadrantChart',
|
||||
'xyChart',
|
||||
'requirement',
|
||||
'mindmap',
|
||||
'timeline',
|
||||
@@ -207,6 +209,7 @@ async function generateTypescript(mermaidConfigSchema: JSONSchemaType<MermaidCon
|
||||
{
|
||||
additionalProperties: false, // in JSON Schema 2019-09, these are called `unevaluatedProperties`
|
||||
unreachableDefinitions: true, // definition for FontConfig is unreachable
|
||||
style: (await prettier.resolveConfig('.')) ?? {},
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
3
packages/mermaid/scripts/docs.cli.mts
Normal file
3
packages/mermaid/scripts/docs.cli.mts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { processDocs } from './docs.mjs';
|
||||
|
||||
void processDocs();
|
||||
@@ -27,14 +27,21 @@
|
||||
* get their absolute paths. Ensures that the location of those 2 directories is not dependent on
|
||||
* where this file resides.
|
||||
*
|
||||
* @todo Write a test file for this. (Will need to be able to deal .mts file. Jest has trouble with
|
||||
* it.)
|
||||
*/
|
||||
// @ts-ignore: we're importing internal jsonschema2md functions
|
||||
import { default as schemaLoader } from '@adobe/jsonschema2md/lib/schemaProxy.js';
|
||||
// @ts-ignore: we're importing internal jsonschema2md functions
|
||||
import { default as traverseSchemas } from '@adobe/jsonschema2md/lib/traverseSchema.js';
|
||||
// @ts-ignore: we're importing internal jsonschema2md functions
|
||||
import { default as buildMarkdownFromSchema } from '@adobe/jsonschema2md/lib/markdownBuilder.js';
|
||||
// @ts-ignore: we're importing internal jsonschema2md functions
|
||||
import { default as jsonSchemaReadmeBuilder } from '@adobe/jsonschema2md/lib/readmeBuilder.js';
|
||||
import { readFileSync, writeFileSync, mkdirSync, existsSync, rmSync, rmdirSync } from 'fs';
|
||||
import { exec } from 'child_process';
|
||||
import { globby } from 'globby';
|
||||
import { JSDOM } from 'jsdom';
|
||||
import type { Code, ListItem, Root, Text } from 'mdast';
|
||||
import { dump, load, JSON_SCHEMA } from 'js-yaml';
|
||||
import type { Code, ListItem, Root, Text, YAML } from 'mdast';
|
||||
import { posix, dirname, relative, join } from 'path';
|
||||
import prettier from 'prettier';
|
||||
import { remark } from 'remark';
|
||||
@@ -46,9 +53,9 @@ import mm from 'micromatch';
|
||||
import flatmap from 'unist-util-flatmap';
|
||||
import { visit } from 'unist-util-visit';
|
||||
|
||||
const MERMAID_MAJOR_VERSION = (
|
||||
JSON.parse(readFileSync('../mermaid/package.json', 'utf8')).version as string
|
||||
).split('.')[0];
|
||||
export const MERMAID_RELEASE_VERSION = JSON.parse(readFileSync('../mermaid/package.json', 'utf8'))
|
||||
.version as string;
|
||||
const MERMAID_MAJOR_VERSION = MERMAID_RELEASE_VERSION.split('.')[0];
|
||||
const CDN_URL = 'https://cdn.jsdelivr.net/npm'; // 'https://unpkg.com';
|
||||
|
||||
const MERMAID_KEYWORD = 'mermaid';
|
||||
@@ -71,7 +78,7 @@ const vitepress: boolean = process.argv.includes('--vitepress');
|
||||
const noHeader: boolean = process.argv.includes('--noHeader') || vitepress;
|
||||
|
||||
// These paths are from the root of the mono-repo, not from the mermaid subdirectory
|
||||
const SOURCE_DOCS_DIR = 'src/docs';
|
||||
export const SOURCE_DOCS_DIR = 'src/docs';
|
||||
const FINAL_DOCS_DIR = vitepress ? 'src/vitepress' : '../../docs';
|
||||
|
||||
const LOGMSG_TRANSFORMED = 'transformed';
|
||||
@@ -158,7 +165,7 @@ const copyTransformedContents = (filename: string, doCopy = false, transformedCo
|
||||
logWasOrShouldBeTransformed(fileInFinalDocDir, doCopy);
|
||||
};
|
||||
|
||||
const readSyncedUTF8file = (filename: string): string => {
|
||||
export const readSyncedUTF8file = (filename: string): string => {
|
||||
return readFileSync(filename, 'utf8');
|
||||
};
|
||||
|
||||
@@ -209,6 +216,8 @@ interface TransformMarkdownAstOptions {
|
||||
originalFilename: string;
|
||||
/** If `true`, add a warning that the file is autogenerated */
|
||||
addAutogeneratedWarning?: boolean;
|
||||
/** If `true`, adds an `editLink: "https://..."` YAML frontmatter field */
|
||||
addEditLink?: boolean;
|
||||
/**
|
||||
* If `true`, remove the YAML metadata from the Markdown input.
|
||||
* Generally, YAML metadata is only used for Vitepress.
|
||||
@@ -231,6 +240,7 @@ interface TransformMarkdownAstOptions {
|
||||
export function transformMarkdownAst({
|
||||
originalFilename,
|
||||
addAutogeneratedWarning,
|
||||
addEditLink,
|
||||
removeYAML,
|
||||
}: TransformMarkdownAstOptions) {
|
||||
return (tree: Root, _file?: any): Root => {
|
||||
@@ -270,6 +280,27 @@ export function transformMarkdownAst({
|
||||
}
|
||||
}
|
||||
|
||||
if (addEditLink) {
|
||||
// add originalFilename as custom editLink in YAML frontmatter
|
||||
let yamlFrontMatter: YAML;
|
||||
if (astWithTransformedBlocks.children[0].type === 'yaml') {
|
||||
yamlFrontMatter = astWithTransformedBlocks.children[0];
|
||||
} else {
|
||||
yamlFrontMatter = {
|
||||
type: 'yaml',
|
||||
value: '',
|
||||
};
|
||||
astWithTransformedBlocks.children.unshift(yamlFrontMatter);
|
||||
}
|
||||
const filePathFromRoot = posix.join('packages/mermaid', originalFilename);
|
||||
yamlFrontMatter.value = dump({
|
||||
...(load(yamlFrontMatter.value, { schema: JSON_SCHEMA }) as
|
||||
| Record<string, unknown>
|
||||
| undefined),
|
||||
editLink: `https://github.com/mermaid-js/mermaid/edit/develop/${filePathFromRoot}`,
|
||||
});
|
||||
}
|
||||
|
||||
if (removeYAML) {
|
||||
const firstNode = astWithTransformedBlocks.children[0];
|
||||
if (firstNode.type == 'yaml') {
|
||||
@@ -306,6 +337,7 @@ const transformMarkdown = (file: string) => {
|
||||
// mermaid project specific plugin
|
||||
originalFilename: file,
|
||||
addAutogeneratedWarning: !noHeader,
|
||||
addEditLink: noHeader,
|
||||
removeYAML: !noHeader,
|
||||
})
|
||||
.processSync(doc)
|
||||
@@ -323,20 +355,10 @@ const transformMarkdown = (file: string) => {
|
||||
copyTransformedContents(file, !verifyOnly, formatted);
|
||||
};
|
||||
|
||||
import { load, JSON_SCHEMA } from 'js-yaml';
|
||||
// @ts-ignore: we're importing internal jsonschema2md functions
|
||||
import { default as schemaLoader } from '@adobe/jsonschema2md/lib/schemaProxy.js';
|
||||
// @ts-ignore: we're importing internal jsonschema2md functions
|
||||
import { default as traverseSchemas } from '@adobe/jsonschema2md/lib/traverseSchema.js';
|
||||
// @ts-ignore: we're importing internal jsonschema2md functions
|
||||
import { default as buildMarkdownFromSchema } from '@adobe/jsonschema2md/lib/markdownBuilder.js';
|
||||
// @ts-ignore: we're importing internal jsonschema2md functions
|
||||
import { default as jsonSchemaReadmeBuilder } from '@adobe/jsonschema2md/lib/readmeBuilder.js';
|
||||
|
||||
/**
|
||||
* Transforms the given JSON Schema into Markdown documentation
|
||||
*/
|
||||
async function transormJsonSchema(file: string) {
|
||||
async function transformJsonSchema(file: string) {
|
||||
const yamlContents = readSyncedUTF8file(file);
|
||||
const jsonSchema = load(yamlContents, {
|
||||
filename: file,
|
||||
@@ -420,16 +442,18 @@ async function transormJsonSchema(file: string) {
|
||||
}
|
||||
});
|
||||
|
||||
const transformed = remark()
|
||||
const transformer = remark()
|
||||
.use(remarkGfm)
|
||||
.use(remarkFrontmatter, ['yaml']) // support YAML front-matter in Markdown
|
||||
.use(transformMarkdownAst, {
|
||||
// mermaid project specific plugin
|
||||
originalFilename: file,
|
||||
addAutogeneratedWarning: !noHeader,
|
||||
addEditLink: noHeader,
|
||||
removeYAML: !noHeader,
|
||||
})
|
||||
.stringify(markdownAst as Root);
|
||||
});
|
||||
|
||||
const transformed = transformer.stringify(await transformer.run(markdownAst as Root));
|
||||
|
||||
const formatted = prettier.format(transformed, {
|
||||
parser: 'markdown',
|
||||
@@ -480,7 +504,7 @@ const transformHtml = (filename: string) => {
|
||||
copyTransformedContents(filename, !verifyOnly, formattedHTML);
|
||||
};
|
||||
|
||||
const getGlobs = (globs: string[]): string[] => {
|
||||
export const getGlobs = (globs: string[]): string[] => {
|
||||
globs.push('!**/dist/**', '!**/redirect.spec.ts', '!**/landing/**', '!**/node_modules/**');
|
||||
if (!vitepress) {
|
||||
globs.push(
|
||||
@@ -494,12 +518,12 @@ const getGlobs = (globs: string[]): string[] => {
|
||||
return globs;
|
||||
};
|
||||
|
||||
const getFilesFromGlobs = async (globs: string[]): Promise<string[]> => {
|
||||
export const getFilesFromGlobs = async (globs: string[]): Promise<string[]> => {
|
||||
return await globby(globs, { dot: true });
|
||||
};
|
||||
|
||||
/** Main method (entry point) */
|
||||
const main = async () => {
|
||||
export const processDocs = async () => {
|
||||
if (verifyOnly) {
|
||||
console.log('Verifying that all files are in sync with the source files');
|
||||
}
|
||||
@@ -509,7 +533,7 @@ const main = async () => {
|
||||
|
||||
if (vitepress) {
|
||||
console.log(`${action} 1 .schema.yaml file`);
|
||||
await transormJsonSchema('src/schemas/config.schema.yaml');
|
||||
await transformJsonSchema('src/schemas/config.schema.yaml');
|
||||
} else {
|
||||
// skip because this creates so many Markdown files that it lags git
|
||||
console.log('Skipping 1 .schema.yaml file');
|
||||
@@ -577,5 +601,3 @@ const main = async () => {
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
void main();
|
||||
@@ -105,6 +105,29 @@ This Markdown should be kept.
|
||||
});
|
||||
});
|
||||
|
||||
it('should add an editLink in the YAML frontmatter if `addEditLink: true`', async () => {
|
||||
const contents = `---
|
||||
title: Flowcharts Syntax
|
||||
---
|
||||
|
||||
This Markdown should be kept.
|
||||
`;
|
||||
const withYaml = (
|
||||
await remarkBuilder()
|
||||
.use(transformMarkdownAst, { originalFilename, addEditLink: true })
|
||||
.process(contents)
|
||||
).toString();
|
||||
expect(withYaml).toEqual(`---
|
||||
title: Flowcharts Syntax
|
||||
editLink: >-
|
||||
https://github.com/mermaid-js/mermaid/edit/develop/packages/mermaid/example-input-filename.md
|
||||
|
||||
---
|
||||
|
||||
This Markdown should be kept.
|
||||
`);
|
||||
});
|
||||
|
||||
describe('transformToBlockQuote', () => {
|
||||
// TODO Is there a way to test this with --vitepress given as a process argument?
|
||||
|
||||
53
packages/mermaid/scripts/update-release-version.mts
Normal file
53
packages/mermaid/scripts/update-release-version.mts
Normal file
@@ -0,0 +1,53 @@
|
||||
/* eslint-disable no-console */
|
||||
|
||||
/**
|
||||
* @file Update the MERMAID_RELEASE_VERSION placeholder in the documentation source files with the current version of mermaid.
|
||||
* So contributors adding new features will only have to add the placeholder and not worry about updating the version number.
|
||||
*
|
||||
*/
|
||||
import { posix } from 'path';
|
||||
import {
|
||||
getGlobs,
|
||||
getFilesFromGlobs,
|
||||
SOURCE_DOCS_DIR,
|
||||
readSyncedUTF8file,
|
||||
MERMAID_RELEASE_VERSION,
|
||||
} from './docs.mjs';
|
||||
import { writeFile } from 'fs/promises';
|
||||
|
||||
const verifyOnly: boolean = process.argv.includes('--verify');
|
||||
const versionPlaceholder = '<MERMAID_RELEASE_VERSION>';
|
||||
|
||||
const main = async () => {
|
||||
const sourceDirGlob = posix.join('.', SOURCE_DOCS_DIR, '**');
|
||||
const mdFileGlobs = getGlobs([posix.join(sourceDirGlob, '*.md')]);
|
||||
mdFileGlobs.push('!**/community/development.md', '!**/community/code.md');
|
||||
const mdFiles = await getFilesFromGlobs(mdFileGlobs);
|
||||
mdFiles.sort();
|
||||
const mdFilesWithPlaceholder: string[] = [];
|
||||
for (const mdFile of mdFiles) {
|
||||
const content = readSyncedUTF8file(mdFile);
|
||||
if (content.includes(versionPlaceholder)) {
|
||||
mdFilesWithPlaceholder.push(mdFile);
|
||||
}
|
||||
}
|
||||
|
||||
if (mdFilesWithPlaceholder.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (verifyOnly) {
|
||||
console.log(
|
||||
`${mdFilesWithPlaceholder.length} file(s) were found with the placeholder ${versionPlaceholder}. Run \`pnpm --filter mermaid docs:release-version\` to update them.`
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
for (const mdFile of mdFilesWithPlaceholder) {
|
||||
const content = readSyncedUTF8file(mdFile);
|
||||
const newContent = content.replace(versionPlaceholder, MERMAID_RELEASE_VERSION);
|
||||
await writeFile(mdFile, newContent);
|
||||
}
|
||||
};
|
||||
|
||||
void main();
|
||||
@@ -2,10 +2,9 @@ import * as configApi from './config.js';
|
||||
import { log } from './logger.js';
|
||||
import { getDiagram, registerDiagram } from './diagram-api/diagramAPI.js';
|
||||
import { detectType, getDiagramLoader } from './diagram-api/detectType.js';
|
||||
import { extractFrontMatter } from './diagram-api/frontmatter.js';
|
||||
import { UnknownDiagramError } from './errors.js';
|
||||
import { DetailedError } from './utils.js';
|
||||
import { cleanupComments } from './diagram-api/comments.js';
|
||||
import type { DetailedError } from './utils.js';
|
||||
import type { DiagramDefinition, DiagramMetadata } from './diagram-api/types.js';
|
||||
|
||||
export type ParseErrorFunction = (err: string | DetailedError | unknown, hash?: any) => void;
|
||||
|
||||
@@ -15,11 +14,13 @@ export type ParseErrorFunction = (err: string | DetailedError | unknown, hash?:
|
||||
*/
|
||||
export class Diagram {
|
||||
type = 'graph';
|
||||
parser;
|
||||
renderer;
|
||||
db;
|
||||
parser: DiagramDefinition['parser'];
|
||||
renderer: DiagramDefinition['renderer'];
|
||||
db: DiagramDefinition['db'];
|
||||
private init?: DiagramDefinition['init'];
|
||||
|
||||
private detectError?: UnknownDiagramError;
|
||||
constructor(public text: string) {
|
||||
constructor(public text: string, public metadata: Pick<DiagramMetadata, 'title'> = {}) {
|
||||
this.text += '\n';
|
||||
const cnf = configApi.getConfig();
|
||||
try {
|
||||
@@ -32,27 +33,10 @@ export class Diagram {
|
||||
log.debug('Type ' + this.type);
|
||||
// Setup diagram
|
||||
this.db = diagram.db;
|
||||
this.db.clear?.();
|
||||
this.renderer = diagram.renderer;
|
||||
this.parser = diagram.parser;
|
||||
const originalParse = this.parser.parse.bind(this.parser);
|
||||
// Wrap the jison parse() method to handle extracting frontmatter.
|
||||
//
|
||||
// This can't be done in this.parse() because some code
|
||||
// directly calls diagram.parser.parse(), bypassing this.parse().
|
||||
//
|
||||
// Similarly, we can't do this in getDiagramFromText() because some code
|
||||
// calls diagram.db.clear(), which would reset anything set by
|
||||
// extractFrontMatter().
|
||||
|
||||
this.parser.parse = (text: string) =>
|
||||
originalParse(cleanupComments(extractFrontMatter(text, this.db)));
|
||||
|
||||
this.parser.parser.yy = this.db;
|
||||
if (diagram.init) {
|
||||
diagram.init(cnf);
|
||||
log.info('Initialized diagram ' + this.type, cnf);
|
||||
}
|
||||
this.init = diagram.init;
|
||||
this.parse();
|
||||
}
|
||||
|
||||
@@ -61,11 +45,17 @@ export class Diagram {
|
||||
throw this.detectError;
|
||||
}
|
||||
this.db.clear?.();
|
||||
const config = configApi.getConfig();
|
||||
this.init?.(config);
|
||||
// This block was added for legacy compatibility. Use frontmatter instead of adding more special cases.
|
||||
if (this.metadata.title) {
|
||||
this.db.setDiagramTitle?.(this.metadata.title);
|
||||
}
|
||||
this.parser.parse(this.text);
|
||||
}
|
||||
|
||||
async render(id: string, version: string) {
|
||||
await this.renderer.draw(this.text, id, version, this);
|
||||
await this.renderer.draw(this.text, id, version, this, null);
|
||||
}
|
||||
|
||||
getParser() {
|
||||
@@ -82,11 +72,15 @@ export class Diagram {
|
||||
* **Warning:** This function may be changed in the future.
|
||||
* @alpha
|
||||
* @param text - The mermaid diagram definition.
|
||||
* @param metadata - Diagram metadata, defined in YAML.
|
||||
* @returns A the Promise of a Diagram object.
|
||||
* @throws {@link UnknownDiagramError} if the diagram type can not be found.
|
||||
* @privateRemarks This is exported as part of the public mermaidAPI.
|
||||
*/
|
||||
export const getDiagramFromText = async (text: string): Promise<Diagram> => {
|
||||
export const getDiagramFromText = async (
|
||||
text: string,
|
||||
metadata: Pick<DiagramMetadata, 'title'> = {}
|
||||
): Promise<Diagram> => {
|
||||
const type = detectType(text, configApi.getConfig());
|
||||
try {
|
||||
// Trying to find the diagram
|
||||
@@ -101,5 +95,5 @@ export const getDiagramFromText = async (text: string): Promise<Diagram> => {
|
||||
const { id, diagram } = await loader();
|
||||
registerDiagram(id, diagram);
|
||||
}
|
||||
return new Diagram(text);
|
||||
return new Diagram(text, metadata);
|
||||
};
|
||||
|
||||
@@ -13,7 +13,6 @@ export const mermaidAPI = {
|
||||
svg: '<svg></svg>',
|
||||
}),
|
||||
parse: mAPI.parse,
|
||||
parseDirective: vi.fn(),
|
||||
initialize: vi.fn(),
|
||||
getConfig: configApi.getConfig,
|
||||
setConfig: configApi.setConfig,
|
||||
|
||||
@@ -1,32 +1,36 @@
|
||||
'use strict';
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
/**
|
||||
* @function assignWithDepth Extends the functionality of {@link ObjectConstructor.assign} with the
|
||||
* assignWithDepth Extends the functionality of {@link Object.assign} with the
|
||||
* ability to merge arbitrary-depth objects For each key in src with path `k` (recursively)
|
||||
* performs an Object.assign(dst[`k`], src[`k`]) with a slight change from the typical handling of
|
||||
* undefined for dst[`k`]: instead of raising an error, dst[`k`] is auto-initialized to {} and
|
||||
* undefined for dst[`k`]: instead of raising an error, dst[`k`] is auto-initialized to `{}` and
|
||||
* effectively merged with src[`k`]<p> Additionally, dissimilar types will not clobber unless the
|
||||
* config.clobber parameter === true. Example:
|
||||
*
|
||||
* ```js
|
||||
* let config_0 = { foo: { bar: 'bar' }, bar: 'foo' };
|
||||
* let config_1 = { foo: 'foo', bar: 'bar' };
|
||||
* let result = assignWithDepth(config_0, config_1);
|
||||
* console.log(result);
|
||||
* //-> result: { foo: { bar: 'bar' }, bar: 'bar' }
|
||||
* ```
|
||||
* ```
|
||||
* const config_0 = { foo: { bar: 'bar' }, bar: 'foo' };
|
||||
* const config_1 = { foo: 'foo', bar: 'bar' };
|
||||
* const result = assignWithDepth(config_0, config_1);
|
||||
* console.log(result);
|
||||
* //-> result: { foo: { bar: 'bar' }, bar: 'bar' }
|
||||
* ```
|
||||
*
|
||||
* Traditional Object.assign would have clobbered foo in config_0 with foo in config_1. If src is a
|
||||
* destructured array of objects and dst is not an array, assignWithDepth will apply each element
|
||||
* of src to dst in order.
|
||||
* @param {any} dst - The destination of the merge
|
||||
* @param {any} src - The source object(s) to merge into destination
|
||||
* @param {{ depth: number; clobber: boolean }} [config] - Depth: depth
|
||||
* to traverse within src and dst for merging - clobber: should dissimilar types clobber (default:
|
||||
* { depth: 2, clobber: false }). Default is `{ depth: 2, clobber: false }`
|
||||
* @returns {any}
|
||||
* @param dst - The destination of the merge
|
||||
* @param src - The source object(s) to merge into destination
|
||||
* @param config -
|
||||
* * depth: depth to traverse within src and dst for merging
|
||||
* * clobber: should dissimilar types clobber
|
||||
*/
|
||||
const assignWithDepth = function (dst, src, config) {
|
||||
const { depth, clobber } = Object.assign({ depth: 2, clobber: false }, config);
|
||||
const assignWithDepth = (
|
||||
dst: any,
|
||||
src: any,
|
||||
{ depth = 2, clobber = false }: { depth?: number; clobber?: boolean } = {}
|
||||
): any => {
|
||||
const config: { depth: number; clobber: boolean } = { depth, clobber };
|
||||
if (Array.isArray(src) && !Array.isArray(dst)) {
|
||||
src.forEach((s) => assignWithDepth(dst, s, config));
|
||||
return dst;
|
||||
@@ -1,47 +0,0 @@
|
||||
import { sanitizeText as _sanitizeText } from './diagrams/common/common.js';
|
||||
import { getConfig } from './config.js';
|
||||
let title = '';
|
||||
let diagramTitle = '';
|
||||
let description = '';
|
||||
|
||||
const sanitizeText = (txt: string): string => _sanitizeText(txt, getConfig());
|
||||
|
||||
export const clear = function (): void {
|
||||
title = '';
|
||||
description = '';
|
||||
diagramTitle = '';
|
||||
};
|
||||
|
||||
export const setAccTitle = function (txt: string): void {
|
||||
title = sanitizeText(txt).replace(/^\s+/g, '');
|
||||
};
|
||||
|
||||
export const getAccTitle = function (): string {
|
||||
return title || diagramTitle;
|
||||
};
|
||||
|
||||
export const setAccDescription = function (txt: string): void {
|
||||
description = sanitizeText(txt).replace(/\n\s+/g, '\n');
|
||||
};
|
||||
|
||||
export const getAccDescription = function (): string {
|
||||
return description;
|
||||
};
|
||||
|
||||
export const setDiagramTitle = function (txt: string): void {
|
||||
diagramTitle = sanitizeText(txt);
|
||||
};
|
||||
|
||||
export const getDiagramTitle = function (): string {
|
||||
return diagramTitle;
|
||||
};
|
||||
|
||||
export default {
|
||||
getAccTitle,
|
||||
setAccTitle,
|
||||
getDiagramTitle,
|
||||
setDiagramTitle,
|
||||
getAccDescription,
|
||||
setAccDescription,
|
||||
clear,
|
||||
};
|
||||
@@ -1,11 +1,13 @@
|
||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
import * as configApi from './config.js';
|
||||
import type { MermaidConfig } from './config.type.js';
|
||||
|
||||
describe('when working with site config', function () {
|
||||
describe('when working with site config', () => {
|
||||
beforeEach(() => {
|
||||
// Resets the site config to default config
|
||||
configApi.setSiteConfig({});
|
||||
});
|
||||
it('should set site config and config properly', function () {
|
||||
it('should set site config and config properly', () => {
|
||||
const config_0 = { fontFamily: 'foo-font', fontSize: 150 };
|
||||
configApi.setSiteConfig(config_0);
|
||||
const config_1 = configApi.getSiteConfig();
|
||||
@@ -14,19 +16,26 @@ describe('when working with site config', function () {
|
||||
expect(config_1.fontSize).toEqual(config_0.fontSize);
|
||||
expect(config_1).toEqual(config_2);
|
||||
});
|
||||
it('should respect secure keys when applying directives', function () {
|
||||
const config_0 = {
|
||||
it('should respect secure keys when applying directives', () => {
|
||||
const config_0: MermaidConfig = {
|
||||
fontFamily: 'foo-font',
|
||||
securityLevel: 'strict', // can't be changed
|
||||
fontSize: 12345, // can't be changed
|
||||
secure: [...configApi.defaultConfig.secure!, 'fontSize'],
|
||||
};
|
||||
configApi.setSiteConfig(config_0);
|
||||
const directive = { fontFamily: 'baf', fontSize: 54321 /* fontSize shouldn't be changed */ };
|
||||
const cfg = configApi.updateCurrentConfig(config_0, [directive]);
|
||||
const directive: MermaidConfig = {
|
||||
fontFamily: 'baf',
|
||||
// fontSize and securityLevel shouldn't be changed
|
||||
fontSize: 54321,
|
||||
securityLevel: 'loose',
|
||||
};
|
||||
const cfg: MermaidConfig = configApi.updateCurrentConfig(config_0, [directive]);
|
||||
expect(cfg.fontFamily).toEqual(directive.fontFamily);
|
||||
expect(cfg.fontSize).toBe(config_0.fontSize);
|
||||
expect(cfg.securityLevel).toBe(config_0.securityLevel);
|
||||
});
|
||||
it('should allow setting partial options', function () {
|
||||
it('should allow setting partial options', () => {
|
||||
const defaultConfig = configApi.getConfig();
|
||||
|
||||
configApi.setConfig({
|
||||
@@ -42,7 +51,7 @@ describe('when working with site config', function () {
|
||||
updatedConfig.quadrantChart!.chartWidth
|
||||
);
|
||||
});
|
||||
it('should set reset config properly', function () {
|
||||
it('should set reset config properly', () => {
|
||||
const config_0 = { fontFamily: 'foo-font', fontSize: 150 };
|
||||
configApi.setSiteConfig(config_0);
|
||||
const config_1 = { fontFamily: 'baf' };
|
||||
@@ -55,7 +64,7 @@ describe('when working with site config', function () {
|
||||
const config_4 = configApi.getSiteConfig();
|
||||
expect(config_4.fontFamily).toEqual(config_0.fontFamily);
|
||||
});
|
||||
it('should set global reset config properly', function () {
|
||||
it('should set global reset config properly', () => {
|
||||
const config_0 = { fontFamily: 'foo-font', fontSize: 150 };
|
||||
configApi.setSiteConfig(config_0);
|
||||
const config_1 = configApi.getSiteConfig();
|
||||
|
||||
@@ -3,15 +3,16 @@ import { log } from './logger.js';
|
||||
import theme from './themes/index.js';
|
||||
import config from './defaultConfig.js';
|
||||
import type { MermaidConfig } from './config.type.js';
|
||||
import { sanitizeDirective } from './utils/sanitizeDirective.js';
|
||||
|
||||
export const defaultConfig: MermaidConfig = Object.freeze(config);
|
||||
|
||||
let siteConfig: MermaidConfig = assignWithDepth({}, defaultConfig);
|
||||
let configFromInitialize: MermaidConfig;
|
||||
let directives: any[] = [];
|
||||
let directives: MermaidConfig[] = [];
|
||||
let currentConfig: MermaidConfig = assignWithDepth({}, defaultConfig);
|
||||
|
||||
export const updateCurrentConfig = (siteCfg: MermaidConfig, _directives: any[]) => {
|
||||
export const updateCurrentConfig = (siteCfg: MermaidConfig, _directives: MermaidConfig[]) => {
|
||||
// start with config being the siteConfig
|
||||
let cfg: MermaidConfig = assignWithDepth({}, siteCfg);
|
||||
// let sCfg = assignWithDepth(defaultConfig, siteConfigDelta);
|
||||
@@ -20,7 +21,6 @@ export const updateCurrentConfig = (siteCfg: MermaidConfig, _directives: any[])
|
||||
let sumOfDirectives: MermaidConfig = {};
|
||||
for (const d of _directives) {
|
||||
sanitize(d);
|
||||
|
||||
// Apply the data from the directive where the the overrides the themeVariables
|
||||
sumOfDirectives = assignWithDepth(sumOfDirectives, d);
|
||||
}
|
||||
@@ -111,12 +111,6 @@ export const getSiteConfig = (): MermaidConfig => {
|
||||
* @returns The currentConfig merged with the sanitized conf
|
||||
*/
|
||||
export const setConfig = (conf: MermaidConfig): MermaidConfig => {
|
||||
// sanitize(conf);
|
||||
// Object.keys(conf).forEach(key => {
|
||||
// const manipulator = manipulators[key];
|
||||
// conf[key] = manipulator ? manipulator(conf[key]) : conf[key];
|
||||
// });
|
||||
|
||||
checkConfig(conf);
|
||||
assignWithDepth(currentConfig, conf);
|
||||
|
||||
@@ -150,9 +144,12 @@ export const getConfig = (): MermaidConfig => {
|
||||
* @param options - The potential setConfig parameter
|
||||
*/
|
||||
export const sanitize = (options: any) => {
|
||||
if (!options) {
|
||||
return;
|
||||
}
|
||||
// Checking that options are not in the list of excluded options
|
||||
['secure', ...(siteConfig.secure ?? [])].forEach((key) => {
|
||||
if (options[key] !== undefined) {
|
||||
if (Object.hasOwn(options, key)) {
|
||||
// DO NOT attempt to print options[key] within `${}` as a malicious script
|
||||
// can exploit the logger's attempt to stringify the value and execute arbitrary code
|
||||
log.debug(`Denied attempt to modify a secure key ${key}`, options[key]);
|
||||
@@ -162,7 +159,7 @@ export const sanitize = (options: any) => {
|
||||
|
||||
// Check that there no attempts of prototype pollution
|
||||
Object.keys(options).forEach((key) => {
|
||||
if (key.indexOf('__') === 0) {
|
||||
if (key.startsWith('__')) {
|
||||
delete options[key];
|
||||
}
|
||||
});
|
||||
@@ -188,16 +185,14 @@ export const sanitize = (options: any) => {
|
||||
*
|
||||
* @param directive - The directive to push in
|
||||
*/
|
||||
export const addDirective = (directive: any) => {
|
||||
if (directive.fontFamily) {
|
||||
if (!directive.themeVariables) {
|
||||
directive.themeVariables = { fontFamily: directive.fontFamily };
|
||||
} else {
|
||||
if (!directive.themeVariables.fontFamily) {
|
||||
directive.themeVariables = { fontFamily: directive.fontFamily };
|
||||
}
|
||||
}
|
||||
export const addDirective = (directive: MermaidConfig) => {
|
||||
sanitizeDirective(directive);
|
||||
|
||||
// If the directive has a fontFamily, but no themeVariables, add the fontFamily to the themeVariables
|
||||
if (directive.fontFamily && (!directive.themeVariables || !directive.themeVariables.fontFamily)) {
|
||||
directive.themeVariables = { fontFamily: directive.fontFamily };
|
||||
}
|
||||
|
||||
directives.push(directive);
|
||||
updateCurrentConfig(siteConfig, directives);
|
||||
};
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
/**
|
||||
* Configuration options to pass to the `dompurify` library.
|
||||
*/
|
||||
export type DOMPurifyConfiguration = import("dompurify").Config;
|
||||
export type DOMPurifyConfiguration = import('dompurify').Config;
|
||||
/**
|
||||
* JavaScript function that returns a `FontConfig`.
|
||||
*
|
||||
@@ -39,7 +39,7 @@ export type FontCalculator = () => Partial<FontConfig>;
|
||||
* This interface was referenced by `MermaidConfig`'s JSON-Schema
|
||||
* via the `definition` "SankeyLinkColor".
|
||||
*/
|
||||
export type SankeyLinkColor = "source" | "target" | "gradient";
|
||||
export type SankeyLinkColor = 'source' | 'target' | 'gradient';
|
||||
/**
|
||||
* Controls the alignment of the Sankey diagrams.
|
||||
*
|
||||
@@ -49,7 +49,7 @@ export type SankeyLinkColor = "source" | "target" | "gradient";
|
||||
* This interface was referenced by `MermaidConfig`'s JSON-Schema
|
||||
* via the `definition` "SankeyNodeAlignment".
|
||||
*/
|
||||
export type SankeyNodeAlignment = "left" | "right" | "center" | "justify";
|
||||
export type SankeyNodeAlignment = 'left' | 'right' | 'center' | 'justify';
|
||||
/**
|
||||
* The font size to use
|
||||
*/
|
||||
@@ -61,7 +61,7 @@ export interface MermaidConfig {
|
||||
* You may also use `themeCSS` to override this value.
|
||||
*
|
||||
*/
|
||||
theme?: string | "default" | "forest" | "dark" | "neutral" | "null";
|
||||
theme?: string | 'default' | 'forest' | 'dark' | 'neutral' | 'null';
|
||||
themeVariables?: any;
|
||||
themeCSS?: string;
|
||||
/**
|
||||
@@ -88,12 +88,12 @@ export interface MermaidConfig {
|
||||
| 0
|
||||
| 2
|
||||
| 1
|
||||
| "trace"
|
||||
| "debug"
|
||||
| "info"
|
||||
| "warn"
|
||||
| "error"
|
||||
| "fatal"
|
||||
| 'trace'
|
||||
| 'debug'
|
||||
| 'info'
|
||||
| 'warn'
|
||||
| 'error'
|
||||
| 'fatal'
|
||||
| 3
|
||||
| 4
|
||||
| 5
|
||||
@@ -101,7 +101,7 @@ export interface MermaidConfig {
|
||||
/**
|
||||
* Level of trust for parsed diagram
|
||||
*/
|
||||
securityLevel?: string | "strict" | "loose" | "antiscript" | "sandbox" | undefined;
|
||||
securityLevel?: string | 'strict' | 'loose' | 'antiscript' | 'sandbox' | undefined;
|
||||
/**
|
||||
* Dictates whether mermaid starts on Page load
|
||||
*/
|
||||
@@ -158,6 +158,7 @@ export interface MermaidConfig {
|
||||
er?: ErDiagramConfig;
|
||||
pie?: PieDiagramConfig;
|
||||
quadrantChart?: QuadrantChartConfig;
|
||||
xyChart?: XYChartConfig;
|
||||
requirement?: RequirementDiagramConfig;
|
||||
mindmap?: MindmapDiagramConfig;
|
||||
gitGraph?: GitGraphDiagramConfig;
|
||||
@@ -697,11 +698,11 @@ export interface QuadrantChartConfig extends BaseDiagramConfig {
|
||||
/**
|
||||
* position of x-axis labels
|
||||
*/
|
||||
xAxisPosition?: "top" | "bottom";
|
||||
xAxisPosition?: 'top' | 'bottom';
|
||||
/**
|
||||
* position of y-axis labels
|
||||
*/
|
||||
yAxisPosition?: "left" | "right";
|
||||
yAxisPosition?: 'left' | 'right';
|
||||
/**
|
||||
* stroke width of edges of the box that are inside the quadrant
|
||||
*/
|
||||
@@ -711,6 +712,194 @@ export interface QuadrantChartConfig extends BaseDiagramConfig {
|
||||
*/
|
||||
quadrantExternalBorderStrokeWidth?: number;
|
||||
}
|
||||
/**
|
||||
* This object contains configuration for XYChart axis config
|
||||
*
|
||||
* This interface was referenced by `MermaidConfig`'s JSON-Schema
|
||||
* via the `definition` "XYChartAxisConfig".
|
||||
*/
|
||||
export interface XYChartAxisConfig {
|
||||
/**
|
||||
* Should show the axis labels (tick text)
|
||||
*/
|
||||
showLabel?: boolean;
|
||||
/**
|
||||
* font size of the axis labels (tick text)
|
||||
*/
|
||||
labelFontSize?: number;
|
||||
/**
|
||||
* top and bottom space from axis label (tick text)
|
||||
*/
|
||||
labelPadding?: number;
|
||||
/**
|
||||
* Should show the axis title
|
||||
*/
|
||||
showTitle?: boolean;
|
||||
/**
|
||||
* font size of the axis title
|
||||
*/
|
||||
titleFontSize?: number;
|
||||
/**
|
||||
* top and bottom space from axis title
|
||||
*/
|
||||
titlePadding?: number;
|
||||
/**
|
||||
* Should show the axis tick lines
|
||||
*/
|
||||
showTick?: boolean;
|
||||
/**
|
||||
* length of the axis tick lines
|
||||
*/
|
||||
tickLength?: number;
|
||||
/**
|
||||
* width of the axis tick lines
|
||||
*/
|
||||
tickWidth?: number;
|
||||
/**
|
||||
* Show line across the axis
|
||||
*/
|
||||
showAxisLine?: boolean;
|
||||
/**
|
||||
* Width of the axis line
|
||||
*/
|
||||
axisLineWidth?: number;
|
||||
}
|
||||
/**
|
||||
* This object contains configuration specific to XYCharts
|
||||
*
|
||||
* This interface was referenced by `MermaidConfig`'s JSON-Schema
|
||||
* via the `definition` "XYChartConfig".
|
||||
*/
|
||||
export interface XYChartConfig extends BaseDiagramConfig {
|
||||
/**
|
||||
* width of the chart
|
||||
*/
|
||||
width?: number;
|
||||
/**
|
||||
* height of the chart
|
||||
*/
|
||||
height?: number;
|
||||
/**
|
||||
* Font size of the chart title
|
||||
*/
|
||||
titleFontSize?: number;
|
||||
/**
|
||||
* Top and bottom space from the chart title
|
||||
*/
|
||||
titlePadding?: number;
|
||||
/**
|
||||
* Should show the chart title
|
||||
*/
|
||||
showTitle?: boolean;
|
||||
xAxis?: XYChartAxisConfig1;
|
||||
yAxis?: XYChartAxisConfig2;
|
||||
/**
|
||||
* How to plot will be drawn horizontal or vertical
|
||||
*/
|
||||
chartOrientation?: 'vertical' | 'horizontal';
|
||||
/**
|
||||
* Minimum percent of space plots of the chart will take
|
||||
*/
|
||||
plotReservedSpacePercent?: number;
|
||||
}
|
||||
/**
|
||||
* This object contains configuration for XYChart axis config
|
||||
*/
|
||||
export interface XYChartAxisConfig1 {
|
||||
/**
|
||||
* Should show the axis labels (tick text)
|
||||
*/
|
||||
showLabel?: boolean;
|
||||
/**
|
||||
* font size of the axis labels (tick text)
|
||||
*/
|
||||
labelFontSize?: number;
|
||||
/**
|
||||
* top and bottom space from axis label (tick text)
|
||||
*/
|
||||
labelPadding?: number;
|
||||
/**
|
||||
* Should show the axis title
|
||||
*/
|
||||
showTitle?: boolean;
|
||||
/**
|
||||
* font size of the axis title
|
||||
*/
|
||||
titleFontSize?: number;
|
||||
/**
|
||||
* top and bottom space from axis title
|
||||
*/
|
||||
titlePadding?: number;
|
||||
/**
|
||||
* Should show the axis tick lines
|
||||
*/
|
||||
showTick?: boolean;
|
||||
/**
|
||||
* length of the axis tick lines
|
||||
*/
|
||||
tickLength?: number;
|
||||
/**
|
||||
* width of the axis tick lines
|
||||
*/
|
||||
tickWidth?: number;
|
||||
/**
|
||||
* Show line across the axis
|
||||
*/
|
||||
showAxisLine?: boolean;
|
||||
/**
|
||||
* Width of the axis line
|
||||
*/
|
||||
axisLineWidth?: number;
|
||||
}
|
||||
/**
|
||||
* This object contains configuration for XYChart axis config
|
||||
*/
|
||||
export interface XYChartAxisConfig2 {
|
||||
/**
|
||||
* Should show the axis labels (tick text)
|
||||
*/
|
||||
showLabel?: boolean;
|
||||
/**
|
||||
* font size of the axis labels (tick text)
|
||||
*/
|
||||
labelFontSize?: number;
|
||||
/**
|
||||
* top and bottom space from axis label (tick text)
|
||||
*/
|
||||
labelPadding?: number;
|
||||
/**
|
||||
* Should show the axis title
|
||||
*/
|
||||
showTitle?: boolean;
|
||||
/**
|
||||
* font size of the axis title
|
||||
*/
|
||||
titleFontSize?: number;
|
||||
/**
|
||||
* top and bottom space from axis title
|
||||
*/
|
||||
titlePadding?: number;
|
||||
/**
|
||||
* Should show the axis tick lines
|
||||
*/
|
||||
showTick?: boolean;
|
||||
/**
|
||||
* length of the axis tick lines
|
||||
*/
|
||||
tickLength?: number;
|
||||
/**
|
||||
* width of the axis tick lines
|
||||
*/
|
||||
tickWidth?: number;
|
||||
/**
|
||||
* Show line across the axis
|
||||
*/
|
||||
showAxisLine?: boolean;
|
||||
/**
|
||||
* Width of the axis line
|
||||
*/
|
||||
axisLineWidth?: number;
|
||||
}
|
||||
/**
|
||||
* The object containing configurations specific for entity relationship diagrams
|
||||
*
|
||||
@@ -731,7 +920,7 @@ export interface ErDiagramConfig extends BaseDiagramConfig {
|
||||
/**
|
||||
* Directional bias for layout of entities
|
||||
*/
|
||||
layoutDirection?: string | "TB" | "BT" | "LR" | "RL";
|
||||
layoutDirection?: string | 'TB' | 'BT' | 'LR' | 'RL';
|
||||
/**
|
||||
* The minimum width of an entity box. Expressed in pixels.
|
||||
*/
|
||||
@@ -796,7 +985,7 @@ export interface StateDiagramConfig extends BaseDiagramConfig {
|
||||
* Decides which rendering engine that is to be used for the rendering.
|
||||
*
|
||||
*/
|
||||
defaultRenderer?: string | "dagre-d3" | "dagre-wrapper" | "elk";
|
||||
defaultRenderer?: string | 'dagre-d3' | 'dagre-wrapper' | 'elk';
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `MermaidConfig`'s JSON-Schema
|
||||
@@ -820,7 +1009,7 @@ export interface ClassDiagramConfig extends BaseDiagramConfig {
|
||||
* Decides which rendering engine that is to be used for the rendering.
|
||||
*
|
||||
*/
|
||||
defaultRenderer?: string | "dagre-d3" | "dagre-wrapper" | "elk";
|
||||
defaultRenderer?: string | 'dagre-d3' | 'dagre-wrapper' | 'elk';
|
||||
nodeSpacing?: number;
|
||||
rankSpacing?: number;
|
||||
/**
|
||||
@@ -880,7 +1069,7 @@ export interface JourneyDiagramConfig extends BaseDiagramConfig {
|
||||
/**
|
||||
* Multiline message alignment
|
||||
*/
|
||||
messageAlign?: string | "left" | "center" | "right";
|
||||
messageAlign?: string | 'left' | 'center' | 'right';
|
||||
/**
|
||||
* Prolongs the edge of the diagram downwards.
|
||||
*
|
||||
@@ -959,7 +1148,7 @@ export interface TimelineDiagramConfig extends BaseDiagramConfig {
|
||||
/**
|
||||
* Multiline message alignment
|
||||
*/
|
||||
messageAlign?: string | "left" | "center" | "right";
|
||||
messageAlign?: string | 'left' | 'center' | 'right';
|
||||
/**
|
||||
* Prolongs the edge of the diagram downwards.
|
||||
*
|
||||
@@ -1056,7 +1245,7 @@ export interface GanttDiagramConfig extends BaseDiagramConfig {
|
||||
* Pattern is:
|
||||
*
|
||||
* ```javascript
|
||||
* /^([1-9][0-9]*)(minute|hour|day|week|month)$/
|
||||
* /^([1-9][0-9]*)(millisecond|second|minute|hour|day|week|month)$/
|
||||
* ```
|
||||
*
|
||||
*/
|
||||
@@ -1070,7 +1259,12 @@ export interface GanttDiagramConfig extends BaseDiagramConfig {
|
||||
* Controls the display mode.
|
||||
*
|
||||
*/
|
||||
displayMode?: string | "compact";
|
||||
displayMode?: string | 'compact';
|
||||
/**
|
||||
* On which day a week-based interval should start
|
||||
*
|
||||
*/
|
||||
weekday?: 'monday' | 'tuesday' | 'wednesday' | 'thursday' | 'friday' | 'saturday' | 'sunday';
|
||||
}
|
||||
/**
|
||||
* The object containing configurations specific for sequence diagrams
|
||||
@@ -1124,7 +1318,7 @@ export interface SequenceDiagramConfig extends BaseDiagramConfig {
|
||||
/**
|
||||
* Multiline message alignment
|
||||
*/
|
||||
messageAlign?: string | "left" | "center" | "right";
|
||||
messageAlign?: string | 'left' | 'center' | 'right';
|
||||
/**
|
||||
* Mirror actors under diagram
|
||||
*
|
||||
@@ -1181,7 +1375,7 @@ export interface SequenceDiagramConfig extends BaseDiagramConfig {
|
||||
/**
|
||||
* This sets the text alignment of actor-attached notes
|
||||
*/
|
||||
noteAlign?: string | "left" | "center" | "right";
|
||||
noteAlign?: string | 'left' | 'center' | 'right';
|
||||
/**
|
||||
* This sets the font size of actor messages
|
||||
*/
|
||||
@@ -1257,7 +1451,7 @@ export interface FlowchartDiagramConfig extends BaseDiagramConfig {
|
||||
* Defines how mermaid renders curves for flowcharts.
|
||||
*
|
||||
*/
|
||||
curve?: string | "basis" | "linear" | "cardinal";
|
||||
curve?: string | 'basis' | 'linear' | 'cardinal';
|
||||
/**
|
||||
* Represents the padding between the labels and the shape
|
||||
*
|
||||
@@ -1269,7 +1463,7 @@ export interface FlowchartDiagramConfig extends BaseDiagramConfig {
|
||||
* Decides which rendering engine that is to be used for the rendering.
|
||||
*
|
||||
*/
|
||||
defaultRenderer?: string | "dagre-d3" | "dagre-wrapper" | "elk";
|
||||
defaultRenderer?: string | 'dagre-d3' | 'dagre-wrapper' | 'elk';
|
||||
/**
|
||||
* Width of nodes where text is wrapped.
|
||||
*
|
||||
@@ -1299,8 +1493,23 @@ export interface SankeyDiagramConfig extends BaseDiagramConfig {
|
||||
* See <https://github.com/d3/d3-sankey#alignments>.
|
||||
*
|
||||
*/
|
||||
nodeAlignment?: "left" | "right" | "center" | "justify";
|
||||
nodeAlignment?: 'left' | 'right' | 'center' | 'justify';
|
||||
useMaxWidth?: boolean;
|
||||
/**
|
||||
* Toggle to display or hide values along with title.
|
||||
*
|
||||
*/
|
||||
showValues?: boolean;
|
||||
/**
|
||||
* The prefix to use for values
|
||||
*
|
||||
*/
|
||||
prefix?: string;
|
||||
/**
|
||||
* The suffix to use for values
|
||||
*
|
||||
*/
|
||||
suffix?: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `MermaidConfig`'s JSON-Schema
|
||||
|
||||
@@ -5,6 +5,7 @@ import { line, curveBasis, select } from 'd3';
|
||||
import { getConfig } from '../config.js';
|
||||
import utils from '../utils.js';
|
||||
import { evaluate } from '../diagrams/common/common.js';
|
||||
import { getLineFunctionsWithOffset } from '../utils/lineWithOffset.js';
|
||||
|
||||
let edgeLabels = {};
|
||||
let terminalLabels = {};
|
||||
@@ -368,8 +369,7 @@ const cutPathAtIntersect = (_points, boundryNode) => {
|
||||
return points;
|
||||
};
|
||||
|
||||
//(edgePaths, e, edge, clusterDb, diagramtype, graph)
|
||||
export const insertEdge = function (elem, e, edge, clusterDb, diagramType, graph) {
|
||||
export const insertEdge = function (elem, e, edge, clusterDb, diagramType, graph, id) {
|
||||
let points = edge.points;
|
||||
let pointsHasChanged = false;
|
||||
const tail = graph.node(e.v);
|
||||
@@ -435,24 +435,16 @@ 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 lineFunction = line()
|
||||
.x(function (d) {
|
||||
return d.x;
|
||||
})
|
||||
.y(function (d) {
|
||||
return d.y;
|
||||
})
|
||||
.curve(curve);
|
||||
|
||||
const { x, y } = getLineFunctionsWithOffset(edge);
|
||||
const lineFunction = line().x(x).y(y).curve(curve);
|
||||
|
||||
// Construct stroke classes based on properties
|
||||
let strokeClasses;
|
||||
@@ -516,61 +508,103 @@ export const insertEdge = function (elem, e, edge, clusterDb, diagramType, graph
|
||||
|
||||
switch (edge.arrowTypeStart) {
|
||||
case 'arrow_cross':
|
||||
svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-crossStart' + ')');
|
||||
svgPath.attr(
|
||||
'marker-start',
|
||||
'url(' + url + '#' + id + '_' + diagramType + '-crossStart' + ')'
|
||||
);
|
||||
break;
|
||||
case 'arrow_point':
|
||||
svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-pointStart' + ')');
|
||||
svgPath.attr(
|
||||
'marker-start',
|
||||
'url(' + url + '#' + id + '_' + diagramType + '-pointStart' + ')'
|
||||
);
|
||||
break;
|
||||
case 'arrow_barb':
|
||||
svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-barbStart' + ')');
|
||||
svgPath.attr(
|
||||
'marker-start',
|
||||
'url(' + url + '#' + id + '_' + diagramType + '-barbStart' + ')'
|
||||
);
|
||||
break;
|
||||
case 'arrow_circle':
|
||||
svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-circleStart' + ')');
|
||||
svgPath.attr(
|
||||
'marker-start',
|
||||
'url(' + url + '#' + id + '_' + diagramType + '-circleStart' + ')'
|
||||
);
|
||||
break;
|
||||
case 'aggregation':
|
||||
svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-aggregationStart' + ')');
|
||||
svgPath.attr(
|
||||
'marker-start',
|
||||
'url(' + url + '#' + id + '_' + diagramType + '-aggregationStart' + ')'
|
||||
);
|
||||
break;
|
||||
case 'extension':
|
||||
svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-extensionStart' + ')');
|
||||
svgPath.attr(
|
||||
'marker-start',
|
||||
'url(' + url + '#' + id + '_' + diagramType + '-extensionStart' + ')'
|
||||
);
|
||||
break;
|
||||
case 'composition':
|
||||
svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-compositionStart' + ')');
|
||||
svgPath.attr(
|
||||
'marker-start',
|
||||
'url(' + url + '#' + id + '_' + diagramType + '-compositionStart' + ')'
|
||||
);
|
||||
break;
|
||||
case 'dependency':
|
||||
svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-dependencyStart' + ')');
|
||||
svgPath.attr(
|
||||
'marker-start',
|
||||
'url(' + url + '#' + id + '_' + diagramType + '-dependencyStart' + ')'
|
||||
);
|
||||
break;
|
||||
case 'lollipop':
|
||||
svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-lollipopStart' + ')');
|
||||
svgPath.attr(
|
||||
'marker-start',
|
||||
'url(' + url + '#' + id + '_' + diagramType + '-lollipopStart' + ')'
|
||||
);
|
||||
break;
|
||||
default:
|
||||
}
|
||||
switch (edge.arrowTypeEnd) {
|
||||
case 'arrow_cross':
|
||||
svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-crossEnd' + ')');
|
||||
svgPath.attr('marker-end', 'url(' + url + '#' + id + '_' + diagramType + '-crossEnd' + ')');
|
||||
break;
|
||||
case 'arrow_point':
|
||||
svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-pointEnd' + ')');
|
||||
svgPath.attr('marker-end', 'url(' + url + '#' + id + '_' + diagramType + '-pointEnd' + ')');
|
||||
break;
|
||||
case 'arrow_barb':
|
||||
svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-barbEnd' + ')');
|
||||
svgPath.attr('marker-end', 'url(' + url + '#' + id + '_' + diagramType + '-barbEnd' + ')');
|
||||
break;
|
||||
case 'arrow_circle':
|
||||
svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-circleEnd' + ')');
|
||||
svgPath.attr('marker-end', 'url(' + url + '#' + id + '_' + diagramType + '-circleEnd' + ')');
|
||||
break;
|
||||
case 'aggregation':
|
||||
svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-aggregationEnd' + ')');
|
||||
svgPath.attr(
|
||||
'marker-end',
|
||||
'url(' + url + '#' + id + '_' + diagramType + '-aggregationEnd' + ')'
|
||||
);
|
||||
break;
|
||||
case 'extension':
|
||||
svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-extensionEnd' + ')');
|
||||
svgPath.attr(
|
||||
'marker-end',
|
||||
'url(' + url + '#' + id + '_' + diagramType + '-extensionEnd' + ')'
|
||||
);
|
||||
break;
|
||||
case 'composition':
|
||||
svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-compositionEnd' + ')');
|
||||
svgPath.attr(
|
||||
'marker-end',
|
||||
'url(' + url + '#' + id + '_' + diagramType + '-compositionEnd' + ')'
|
||||
);
|
||||
break;
|
||||
case 'dependency':
|
||||
svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-dependencyEnd' + ')');
|
||||
svgPath.attr(
|
||||
'marker-end',
|
||||
'url(' + url + '#' + id + '_' + diagramType + '-dependencyEnd' + ')'
|
||||
);
|
||||
break;
|
||||
case 'lollipop':
|
||||
svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-lollipopEnd' + ')');
|
||||
svgPath.attr(
|
||||
'marker-end',
|
||||
'url(' + url + '#' + id + '_' + diagramType + '-lollipopEnd' + ')'
|
||||
);
|
||||
break;
|
||||
default:
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import { insertCluster, clear as clearClusters } from './clusters.js';
|
||||
import { insertEdgeLabel, positionEdgeLabel, insertEdge, clear as clearEdges } from './edges.js';
|
||||
import { log } from '../logger.js';
|
||||
|
||||
const recursiveRender = async (_elem, graph, diagramtype, parentCluster) => {
|
||||
const recursiveRender = async (_elem, graph, diagramtype, id, parentCluster) => {
|
||||
log.info('Graph in recursive render: XXX', graphlibJson.write(graph), parentCluster);
|
||||
const dir = graph.graph().rankdir;
|
||||
log.trace('Dir in recursive render - dir:', dir);
|
||||
@@ -52,7 +52,7 @@ const recursiveRender = async (_elem, graph, diagramtype, parentCluster) => {
|
||||
if (node && node.clusterNode) {
|
||||
// const children = graph.children(v);
|
||||
log.info('Cluster identified', v, node.width, graph.node(v));
|
||||
const o = await recursiveRender(nodes, node.graph, diagramtype, graph.node(v));
|
||||
const o = await recursiveRender(nodes, node.graph, diagramtype, id, graph.node(v));
|
||||
const newEl = o.elem;
|
||||
updateNodeBounds(node, newEl);
|
||||
node.diff = o.diff || 0;
|
||||
@@ -134,7 +134,7 @@ const recursiveRender = async (_elem, graph, diagramtype, parentCluster) => {
|
||||
const edge = graph.edge(e);
|
||||
log.info('Edge ' + e.v + ' -> ' + e.w + ': ' + JSON.stringify(edge), edge);
|
||||
|
||||
const paths = insertEdge(edgePaths, e, edge, clusterDb, diagramtype, graph);
|
||||
const paths = insertEdge(edgePaths, e, edge, clusterDb, diagramtype, graph, id);
|
||||
positionEdgeLabel(edge, paths);
|
||||
});
|
||||
|
||||
@@ -155,11 +155,11 @@ 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 after:', JSON.stringify(graphlibJson.write(graph)));
|
||||
// log.warn('Graph ever after:', graphlibJson.write(graph.node('A').graph));
|
||||
await recursiveRender(elem, graph, diagramtype);
|
||||
await recursiveRender(elem, graph, diagramtype, id);
|
||||
};
|
||||
|
||||
// const shapeDefinitions = {};
|
||||
|
||||
@@ -14,9 +14,9 @@ const extension = (elem, type, id) => {
|
||||
elem
|
||||
.append('defs')
|
||||
.append('marker')
|
||||
.attr('id', type + '-extensionStart')
|
||||
.attr('id', id + '_' + type + '-extensionStart')
|
||||
.attr('class', 'marker extension ' + type)
|
||||
.attr('refX', 0)
|
||||
.attr('refX', 18)
|
||||
.attr('refY', 7)
|
||||
.attr('markerWidth', 190)
|
||||
.attr('markerHeight', 240)
|
||||
@@ -27,9 +27,9 @@ const extension = (elem, type, id) => {
|
||||
elem
|
||||
.append('defs')
|
||||
.append('marker')
|
||||
.attr('id', type + '-extensionEnd')
|
||||
.attr('id', id + '_' + type + '-extensionEnd')
|
||||
.attr('class', 'marker extension ' + type)
|
||||
.attr('refX', 19)
|
||||
.attr('refX', 1)
|
||||
.attr('refY', 7)
|
||||
.attr('markerWidth', 20)
|
||||
.attr('markerHeight', 28)
|
||||
@@ -38,13 +38,13 @@ const extension = (elem, type, id) => {
|
||||
.attr('d', 'M 1,1 V 13 L18,7 Z'); // this is actual shape for arrowhead
|
||||
};
|
||||
|
||||
const composition = (elem, type) => {
|
||||
const composition = (elem, type, id) => {
|
||||
elem
|
||||
.append('defs')
|
||||
.append('marker')
|
||||
.attr('id', type + '-compositionStart')
|
||||
.attr('id', id + '_' + type + '-compositionStart')
|
||||
.attr('class', 'marker composition ' + type)
|
||||
.attr('refX', 0)
|
||||
.attr('refX', 18)
|
||||
.attr('refY', 7)
|
||||
.attr('markerWidth', 190)
|
||||
.attr('markerHeight', 240)
|
||||
@@ -55,9 +55,9 @@ const composition = (elem, type) => {
|
||||
elem
|
||||
.append('defs')
|
||||
.append('marker')
|
||||
.attr('id', type + '-compositionEnd')
|
||||
.attr('id', id + '_' + type + '-compositionEnd')
|
||||
.attr('class', 'marker composition ' + type)
|
||||
.attr('refX', 19)
|
||||
.attr('refX', 1)
|
||||
.attr('refY', 7)
|
||||
.attr('markerWidth', 20)
|
||||
.attr('markerHeight', 28)
|
||||
@@ -65,13 +65,13 @@ const composition = (elem, type) => {
|
||||
.append('path')
|
||||
.attr('d', 'M 18,7 L9,13 L1,7 L9,1 Z');
|
||||
};
|
||||
const aggregation = (elem, type) => {
|
||||
const aggregation = (elem, type, id) => {
|
||||
elem
|
||||
.append('defs')
|
||||
.append('marker')
|
||||
.attr('id', type + '-aggregationStart')
|
||||
.attr('id', id + '_' + type + '-aggregationStart')
|
||||
.attr('class', 'marker aggregation ' + type)
|
||||
.attr('refX', 0)
|
||||
.attr('refX', 18)
|
||||
.attr('refY', 7)
|
||||
.attr('markerWidth', 190)
|
||||
.attr('markerHeight', 240)
|
||||
@@ -82,9 +82,9 @@ const aggregation = (elem, type) => {
|
||||
elem
|
||||
.append('defs')
|
||||
.append('marker')
|
||||
.attr('id', type + '-aggregationEnd')
|
||||
.attr('id', id + '_' + type + '-aggregationEnd')
|
||||
.attr('class', 'marker aggregation ' + type)
|
||||
.attr('refX', 19)
|
||||
.attr('refX', 1)
|
||||
.attr('refY', 7)
|
||||
.attr('markerWidth', 20)
|
||||
.attr('markerHeight', 28)
|
||||
@@ -92,13 +92,13 @@ const aggregation = (elem, type) => {
|
||||
.append('path')
|
||||
.attr('d', 'M 18,7 L9,13 L1,7 L9,1 Z');
|
||||
};
|
||||
const dependency = (elem, type) => {
|
||||
const dependency = (elem, type, id) => {
|
||||
elem
|
||||
.append('defs')
|
||||
.append('marker')
|
||||
.attr('id', type + '-dependencyStart')
|
||||
.attr('id', id + '_' + type + '-dependencyStart')
|
||||
.attr('class', 'marker dependency ' + type)
|
||||
.attr('refX', 0)
|
||||
.attr('refX', 6)
|
||||
.attr('refY', 7)
|
||||
.attr('markerWidth', 190)
|
||||
.attr('markerHeight', 240)
|
||||
@@ -109,9 +109,9 @@ const dependency = (elem, type) => {
|
||||
elem
|
||||
.append('defs')
|
||||
.append('marker')
|
||||
.attr('id', type + '-dependencyEnd')
|
||||
.attr('id', id + '_' + type + '-dependencyEnd')
|
||||
.attr('class', 'marker dependency ' + type)
|
||||
.attr('refX', 19)
|
||||
.attr('refX', 13)
|
||||
.attr('refY', 7)
|
||||
.attr('markerWidth', 20)
|
||||
.attr('markerHeight', 28)
|
||||
@@ -119,31 +119,48 @@ const dependency = (elem, type) => {
|
||||
.append('path')
|
||||
.attr('d', 'M 18,7 L9,13 L14,7 L9,1 Z');
|
||||
};
|
||||
const lollipop = (elem, type) => {
|
||||
const lollipop = (elem, type, id) => {
|
||||
elem
|
||||
.append('defs')
|
||||
.append('marker')
|
||||
.attr('id', type + '-lollipopStart')
|
||||
.attr('id', 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', 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);
|
||||
};
|
||||
const point = (elem, type) => {
|
||||
const point = (elem, type, id) => {
|
||||
elem
|
||||
.append('marker')
|
||||
.attr('id', type + '-pointEnd')
|
||||
.attr('id', id + '_' + type + '-pointEnd')
|
||||
.attr('class', 'marker ' + type)
|
||||
.attr('viewBox', '0 0 10 10')
|
||||
.attr('refX', 10)
|
||||
.attr('refX', 6)
|
||||
.attr('refY', 5)
|
||||
.attr('markerUnits', 'userSpaceOnUse')
|
||||
.attr('markerWidth', 12)
|
||||
@@ -156,10 +173,10 @@ const point = (elem, type) => {
|
||||
.style('stroke-dasharray', '1,0');
|
||||
elem
|
||||
.append('marker')
|
||||
.attr('id', type + '-pointStart')
|
||||
.attr('id', id + '_' + type + '-pointStart')
|
||||
.attr('class', 'marker ' + type)
|
||||
.attr('viewBox', '0 0 10 10')
|
||||
.attr('refX', 0)
|
||||
.attr('refX', 4.5)
|
||||
.attr('refY', 5)
|
||||
.attr('markerUnits', 'userSpaceOnUse')
|
||||
.attr('markerWidth', 12)
|
||||
@@ -171,10 +188,10 @@ const point = (elem, type) => {
|
||||
.style('stroke-width', 1)
|
||||
.style('stroke-dasharray', '1,0');
|
||||
};
|
||||
const circle = (elem, type) => {
|
||||
const circle = (elem, type, id) => {
|
||||
elem
|
||||
.append('marker')
|
||||
.attr('id', type + '-circleEnd')
|
||||
.attr('id', id + '_' + type + '-circleEnd')
|
||||
.attr('class', 'marker ' + type)
|
||||
.attr('viewBox', '0 0 10 10')
|
||||
.attr('refX', 11)
|
||||
@@ -193,7 +210,7 @@ const circle = (elem, type) => {
|
||||
|
||||
elem
|
||||
.append('marker')
|
||||
.attr('id', type + '-circleStart')
|
||||
.attr('id', id + '_' + type + '-circleStart')
|
||||
.attr('class', 'marker ' + type)
|
||||
.attr('viewBox', '0 0 10 10')
|
||||
.attr('refX', -1)
|
||||
@@ -210,10 +227,10 @@ const circle = (elem, type) => {
|
||||
.style('stroke-width', 1)
|
||||
.style('stroke-dasharray', '1,0');
|
||||
};
|
||||
const cross = (elem, type) => {
|
||||
const cross = (elem, type, id) => {
|
||||
elem
|
||||
.append('marker')
|
||||
.attr('id', type + '-crossEnd')
|
||||
.attr('id', id + '_' + type + '-crossEnd')
|
||||
.attr('class', 'marker cross ' + type)
|
||||
.attr('viewBox', '0 0 11 11')
|
||||
.attr('refX', 12)
|
||||
@@ -231,7 +248,7 @@ const cross = (elem, type) => {
|
||||
|
||||
elem
|
||||
.append('marker')
|
||||
.attr('id', type + '-crossStart')
|
||||
.attr('id', id + '_' + type + '-crossStart')
|
||||
.attr('class', 'marker cross ' + type)
|
||||
.attr('viewBox', '0 0 11 11')
|
||||
.attr('refX', -1)
|
||||
@@ -247,11 +264,11 @@ const cross = (elem, type) => {
|
||||
.style('stroke-width', 2)
|
||||
.style('stroke-dasharray', '1,0');
|
||||
};
|
||||
const barb = (elem, type) => {
|
||||
const barb = (elem, type, id) => {
|
||||
elem
|
||||
.append('defs')
|
||||
.append('marker')
|
||||
.attr('id', type + '-barbEnd')
|
||||
.attr('id', id + '_' + type + '-barbEnd')
|
||||
.attr('refX', 19)
|
||||
.attr('refY', 7)
|
||||
.attr('markerWidth', 20)
|
||||
|
||||
@@ -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 = '';
|
||||
|
||||
@@ -5,11 +5,27 @@ 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) => {
|
||||
if (str) {
|
||||
return ' ' + str;
|
||||
}
|
||||
return '';
|
||||
};
|
||||
const getClassesFromNode = (node, otherClasses) => {
|
||||
return `${otherClasses ? otherClasses : 'node default'}${formatClass(node.classes)} ${formatClass(
|
||||
node.class
|
||||
)}`;
|
||||
};
|
||||
|
||||
const question = async (parent, node) => {
|
||||
const { shapeSvg, bbox } = await labelHelper(parent, node, undefined, true);
|
||||
const { shapeSvg, bbox } = await labelHelper(
|
||||
parent,
|
||||
node,
|
||||
getClassesFromNode(node, undefined),
|
||||
true
|
||||
);
|
||||
|
||||
const w = bbox.width + node.padding;
|
||||
const h = bbox.height + node.padding;
|
||||
@@ -70,7 +86,12 @@ const choice = (parent, node) => {
|
||||
};
|
||||
|
||||
const hexagon = async (parent, node) => {
|
||||
const { shapeSvg, bbox } = await labelHelper(parent, node, undefined, true);
|
||||
const { shapeSvg, bbox } = await labelHelper(
|
||||
parent,
|
||||
node,
|
||||
getClassesFromNode(node, undefined),
|
||||
true
|
||||
);
|
||||
|
||||
const f = 4;
|
||||
const h = bbox.height + node.padding;
|
||||
@@ -97,7 +118,12 @@ const hexagon = async (parent, node) => {
|
||||
};
|
||||
|
||||
const rect_left_inv_arrow = async (parent, node) => {
|
||||
const { shapeSvg, bbox } = await labelHelper(parent, node, undefined, true);
|
||||
const { shapeSvg, bbox } = await labelHelper(
|
||||
parent,
|
||||
node,
|
||||
getClassesFromNode(node, undefined),
|
||||
true
|
||||
);
|
||||
|
||||
const w = bbox.width + node.padding;
|
||||
const h = bbox.height + node.padding;
|
||||
@@ -123,7 +149,7 @@ const rect_left_inv_arrow = async (parent, node) => {
|
||||
};
|
||||
|
||||
const lean_right = async (parent, node) => {
|
||||
const { shapeSvg, bbox } = await labelHelper(parent, node, undefined, true);
|
||||
const { shapeSvg, bbox } = await labelHelper(parent, node, getClassesFromNode(node), true);
|
||||
|
||||
const w = bbox.width + node.padding;
|
||||
const h = bbox.height + node.padding;
|
||||
@@ -146,7 +172,12 @@ const lean_right = async (parent, node) => {
|
||||
};
|
||||
|
||||
const lean_left = async (parent, node) => {
|
||||
const { shapeSvg, bbox } = await labelHelper(parent, node, undefined, true);
|
||||
const { shapeSvg, bbox } = await labelHelper(
|
||||
parent,
|
||||
node,
|
||||
getClassesFromNode(node, undefined),
|
||||
true
|
||||
);
|
||||
|
||||
const w = bbox.width + node.padding;
|
||||
const h = bbox.height + node.padding;
|
||||
@@ -169,7 +200,12 @@ const lean_left = async (parent, node) => {
|
||||
};
|
||||
|
||||
const trapezoid = async (parent, node) => {
|
||||
const { shapeSvg, bbox } = await labelHelper(parent, node, undefined, true);
|
||||
const { shapeSvg, bbox } = await labelHelper(
|
||||
parent,
|
||||
node,
|
||||
getClassesFromNode(node, undefined),
|
||||
true
|
||||
);
|
||||
|
||||
const w = bbox.width + node.padding;
|
||||
const h = bbox.height + node.padding;
|
||||
@@ -192,7 +228,12 @@ const trapezoid = async (parent, node) => {
|
||||
};
|
||||
|
||||
const inv_trapezoid = async (parent, node) => {
|
||||
const { shapeSvg, bbox } = await labelHelper(parent, node, undefined, true);
|
||||
const { shapeSvg, bbox } = await labelHelper(
|
||||
parent,
|
||||
node,
|
||||
getClassesFromNode(node, undefined),
|
||||
true
|
||||
);
|
||||
|
||||
const w = bbox.width + node.padding;
|
||||
const h = bbox.height + node.padding;
|
||||
@@ -215,7 +256,12 @@ const inv_trapezoid = async (parent, node) => {
|
||||
};
|
||||
|
||||
const rect_right_inv_arrow = async (parent, node) => {
|
||||
const { shapeSvg, bbox } = await labelHelper(parent, node, undefined, true);
|
||||
const { shapeSvg, bbox } = await labelHelper(
|
||||
parent,
|
||||
node,
|
||||
getClassesFromNode(node, undefined),
|
||||
true
|
||||
);
|
||||
|
||||
const w = bbox.width + node.padding;
|
||||
const h = bbox.height + node.padding;
|
||||
@@ -239,7 +285,12 @@ const rect_right_inv_arrow = async (parent, node) => {
|
||||
};
|
||||
|
||||
const cylinder = async (parent, node) => {
|
||||
const { shapeSvg, bbox } = await labelHelper(parent, node, undefined, true);
|
||||
const { shapeSvg, bbox } = await labelHelper(
|
||||
parent,
|
||||
node,
|
||||
getClassesFromNode(node, undefined),
|
||||
true
|
||||
);
|
||||
|
||||
const w = bbox.width + node.padding;
|
||||
const rx = w / 2;
|
||||
@@ -314,7 +365,7 @@ const rect = async (parent, node) => {
|
||||
const { shapeSvg, bbox, halfPadding } = await labelHelper(
|
||||
parent,
|
||||
node,
|
||||
'node ' + node.classes,
|
||||
'node ' + node.classes + ' ' + node.class,
|
||||
true
|
||||
);
|
||||
|
||||
@@ -360,7 +411,7 @@ const rect = async (parent, node) => {
|
||||
const labelRect = async (parent, node) => {
|
||||
const { shapeSvg } = await labelHelper(parent, node, 'label', true);
|
||||
|
||||
log.trace('Classes = ', node.classes);
|
||||
log.trace('Classes = ', node.class);
|
||||
// add the rect
|
||||
const rect = shapeSvg.insert('rect', ':first-child');
|
||||
|
||||
@@ -545,7 +596,12 @@ const rectWithTitle = (parent, node) => {
|
||||
};
|
||||
|
||||
const stadium = async (parent, node) => {
|
||||
const { shapeSvg, bbox } = await labelHelper(parent, node, undefined, true);
|
||||
const { shapeSvg, bbox } = await labelHelper(
|
||||
parent,
|
||||
node,
|
||||
getClassesFromNode(node, undefined),
|
||||
true
|
||||
);
|
||||
|
||||
const h = bbox.height + node.padding;
|
||||
const w = bbox.width + h / 4 + node.padding;
|
||||
@@ -571,7 +627,12 @@ const stadium = async (parent, node) => {
|
||||
};
|
||||
|
||||
const circle = async (parent, node) => {
|
||||
const { shapeSvg, bbox, halfPadding } = await labelHelper(parent, node, undefined, true);
|
||||
const { shapeSvg, bbox, halfPadding } = await labelHelper(
|
||||
parent,
|
||||
node,
|
||||
getClassesFromNode(node, undefined),
|
||||
true
|
||||
);
|
||||
const circle = shapeSvg.insert('circle', ':first-child');
|
||||
|
||||
// center the circle around its coordinate
|
||||
@@ -596,7 +657,12 @@ const circle = async (parent, node) => {
|
||||
};
|
||||
|
||||
const doublecircle = async (parent, node) => {
|
||||
const { shapeSvg, bbox, halfPadding } = await labelHelper(parent, node, undefined, true);
|
||||
const { shapeSvg, bbox, halfPadding } = await labelHelper(
|
||||
parent,
|
||||
node,
|
||||
getClassesFromNode(node, undefined),
|
||||
true
|
||||
);
|
||||
const gap = 5;
|
||||
const circleGroup = shapeSvg.insert('g', ':first-child');
|
||||
const outerCircle = circleGroup.insert('circle');
|
||||
@@ -634,7 +700,12 @@ const doublecircle = async (parent, node) => {
|
||||
};
|
||||
|
||||
const subroutine = async (parent, node) => {
|
||||
const { shapeSvg, bbox } = await labelHelper(parent, node, undefined, true);
|
||||
const { shapeSvg, bbox } = await labelHelper(
|
||||
parent,
|
||||
node,
|
||||
getClassesFromNode(node, undefined),
|
||||
true
|
||||
);
|
||||
|
||||
const w = bbox.width + node.padding;
|
||||
const h = bbox.height + node.padding;
|
||||
@@ -808,8 +879,8 @@ const class_box = (parent, node) => {
|
||||
maxWidth = classTitleBBox.width;
|
||||
}
|
||||
const classAttributes = [];
|
||||
node.classData.members.forEach((str) => {
|
||||
const parsedInfo = parseMember(str);
|
||||
node.classData.members.forEach((member) => {
|
||||
const parsedInfo = member.getDisplayDetails();
|
||||
let parsedText = parsedInfo.displayText;
|
||||
if (getConfig().flowchart.htmlLabels) {
|
||||
parsedText = parsedText.replace(/</g, '<').replace(/>/g, '>');
|
||||
@@ -842,8 +913,8 @@ const class_box = (parent, node) => {
|
||||
maxHeight += lineHeight;
|
||||
|
||||
const classMethods = [];
|
||||
node.classData.methods.forEach((str) => {
|
||||
const parsedInfo = parseMember(str);
|
||||
node.classData.methods.forEach((member) => {
|
||||
const parsedInfo = member.getDisplayDetails();
|
||||
let displayText = parsedInfo.displayText;
|
||||
if (getConfig().flowchart.htmlLabels) {
|
||||
displayText = displayText.replace(/</g, '<').replace(/>/g, '>');
|
||||
@@ -917,7 +988,9 @@ const class_box = (parent, node) => {
|
||||
((-1 * maxHeight) / 2 + verticalPos + lineHeight / 2) +
|
||||
')'
|
||||
);
|
||||
verticalPos += classTitleBBox.height + rowPadding;
|
||||
//get the height of the bounding box of each member if exists
|
||||
const memberBBox = lbl?.getBBox();
|
||||
verticalPos += (memberBBox?.height ?? 0) + rowPadding;
|
||||
});
|
||||
|
||||
verticalPos += lineHeight;
|
||||
@@ -935,7 +1008,8 @@ const class_box = (parent, node) => {
|
||||
'transform',
|
||||
'translate( ' + -maxWidth / 2 + ', ' + ((-1 * maxHeight) / 2 + verticalPos) + ')'
|
||||
);
|
||||
verticalPos += classTitleBBox.height + rowPadding;
|
||||
const memberBBox = lbl?.getBBox();
|
||||
verticalPos += (memberBBox?.height ?? 0) + rowPadding;
|
||||
});
|
||||
|
||||
rect
|
||||
|
||||
@@ -13,6 +13,7 @@ export const labelHelper = async (parent, node, _classes, isNode) => {
|
||||
} else {
|
||||
classes = _classes;
|
||||
}
|
||||
|
||||
// Add outer g element
|
||||
const shapeSvg = parent
|
||||
.insert('g')
|
||||
@@ -49,7 +50,6 @@ export const labelHelper = async (parent, node, _classes, isNode) => {
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Get the size of the label
|
||||
let bbox = text.getBBox();
|
||||
const halfPadding = node.padding / 2;
|
||||
@@ -66,8 +66,11 @@ export const labelHelper = async (parent, node, _classes, isNode) => {
|
||||
await Promise.all(
|
||||
[...images].map(
|
||||
(img) =>
|
||||
new Promise((res) =>
|
||||
img.addEventListener('load', function () {
|
||||
new Promise((res) => {
|
||||
/**
|
||||
*
|
||||
*/
|
||||
function setupImage() {
|
||||
img.style.display = 'flex';
|
||||
img.style.flexDirection = 'column';
|
||||
|
||||
@@ -82,8 +85,15 @@ export const labelHelper = async (parent, node, _classes, isNode) => {
|
||||
img.style.width = '100%';
|
||||
}
|
||||
res(img);
|
||||
})
|
||||
)
|
||||
}
|
||||
setTimeout(() => {
|
||||
if (img.complete) {
|
||||
setupImage();
|
||||
}
|
||||
});
|
||||
img.addEventListener('error', setupImage);
|
||||
img.addEventListener('load', setupImage);
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { RequiredDeep } from 'type-fest';
|
||||
|
||||
import theme from './themes/index.js';
|
||||
import { type MermaidConfig } from './config.type.js';
|
||||
import type { MermaidConfig } from './config.type.js';
|
||||
|
||||
// Uses our custom Vite jsonSchemaPlugin to load only the default values from
|
||||
// our JSON Schema
|
||||
@@ -13,7 +15,7 @@ import defaultConfigJson from './schemas/config.schema.yaml?only-defaults=true';
|
||||
* Non-JSON JS default values are listed in this file, e.g. functions, or
|
||||
* `undefined` (explicitly set so that `configKeys` finds them).
|
||||
*/
|
||||
const config: Partial<MermaidConfig> = {
|
||||
const config: RequiredDeep<MermaidConfig> = {
|
||||
...defaultConfigJson,
|
||||
// Set, even though they're `undefined` so that `configKeys` finds these keys
|
||||
// TODO: Should we replace these with `null` so that they can go in the JSON Schema?
|
||||
@@ -232,6 +234,10 @@ const config: Partial<MermaidConfig> = {
|
||||
},
|
||||
pie: {
|
||||
...defaultConfigJson.pie,
|
||||
useWidth: 984,
|
||||
},
|
||||
xyChart: {
|
||||
...defaultConfigJson.xyChart,
|
||||
useWidth: undefined,
|
||||
},
|
||||
requirement: {
|
||||
@@ -263,5 +269,5 @@ const keyify = (obj: any, prefix = ''): string[] =>
|
||||
return [...res, prefix + el];
|
||||
}, []);
|
||||
|
||||
export const configKeys: string[] = keyify(config, '');
|
||||
export const configKeys: Set<string> = new Set(keyify(config, ''));
|
||||
export default config;
|
||||
|
||||
@@ -4,5 +4,5 @@
|
||||
* @returns cleaned text
|
||||
*/
|
||||
export const cleanupComments = (text: string): string => {
|
||||
return text.trimStart().replace(/^\s*%%(?!{)[^\n]+\n?/gm, '');
|
||||
return text.replace(/^\s*%%(?!{)[^\n]+\n?/gm, '').trimStart();
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { MermaidConfig } from '../config.type.js';
|
||||
import type { MermaidConfig } from '../config.type.js';
|
||||
import { log } from '../logger.js';
|
||||
import type {
|
||||
DetectorRecord,
|
||||
@@ -6,14 +6,10 @@ import type {
|
||||
DiagramLoader,
|
||||
ExternalDiagramDefinition,
|
||||
} from './types.js';
|
||||
import { frontMatterRegex } from './frontmatter.js';
|
||||
import { getDiagram, registerDiagram } from './diagramAPI.js';
|
||||
import { anyCommentRegex, directiveRegex, frontMatterRegex } from './regexes.js';
|
||||
import { UnknownDiagramError } from '../errors.js';
|
||||
|
||||
const directive = /%{2}{\s*(?:(\w+)\s*:|(\w+))\s*(?:(\w+)|((?:(?!}%{2}).|\r?\n)*))?\s*(?:}%{2})?/gi;
|
||||
const anyComment = /\s*%%.*\n/gm;
|
||||
|
||||
const detectors: Record<string, DetectorRecord> = {};
|
||||
export const detectors: Record<string, DetectorRecord> = {};
|
||||
|
||||
/**
|
||||
* Detects the type of the graph text.
|
||||
@@ -38,7 +34,10 @@ const detectors: Record<string, DetectorRecord> = {};
|
||||
* @returns A graph definition key
|
||||
*/
|
||||
export const detectType = function (text: string, config?: MermaidConfig): string {
|
||||
text = text.replace(frontMatterRegex, '').replace(directive, '').replace(anyComment, '\n');
|
||||
text = text
|
||||
.replace(frontMatterRegex, '')
|
||||
.replace(directiveRegex, '')
|
||||
.replace(anyCommentRegex, '\n');
|
||||
for (const [key, { detector }] of Object.entries(detectors)) {
|
||||
const diagram = detector(text, config);
|
||||
if (diagram) {
|
||||
@@ -70,39 +69,6 @@ export const registerLazyLoadedDiagrams = (...diagrams: ExternalDiagramDefinitio
|
||||
}
|
||||
};
|
||||
|
||||
export const loadRegisteredDiagrams = async () => {
|
||||
log.debug(`Loading registered diagrams`);
|
||||
// Load all lazy loaded diagrams in parallel
|
||||
const results = await Promise.allSettled(
|
||||
Object.entries(detectors).map(async ([key, { detector, loader }]) => {
|
||||
if (loader) {
|
||||
try {
|
||||
getDiagram(key);
|
||||
} catch (error) {
|
||||
try {
|
||||
// Register diagram if it is not already registered
|
||||
const { diagram, id } = await loader();
|
||||
registerDiagram(id, diagram, detector);
|
||||
} catch (err) {
|
||||
// Remove failed diagram from detectors
|
||||
log.error(`Failed to load external diagram with key ${key}. Removing from detectors.`);
|
||||
delete detectors[key];
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
const failed = results.filter((result) => result.status === 'rejected');
|
||||
if (failed.length > 0) {
|
||||
log.error(`Failed to load ${failed.length} external diagrams`);
|
||||
for (const res of failed) {
|
||||
log.error(res);
|
||||
}
|
||||
throw new Error(`Failed to load ${failed.length} external diagrams`);
|
||||
}
|
||||
};
|
||||
|
||||
export const addDetector = (key: string, detector: DiagramDetector, loader?: DiagramLoader) => {
|
||||
if (detectors[key]) {
|
||||
log.error(`Detector with key ${key} already exists`);
|
||||
|
||||
@@ -5,8 +5,9 @@ import er from '../diagrams/er/erDetector.js';
|
||||
import git from '../diagrams/git/gitGraphDetector.js';
|
||||
import gantt from '../diagrams/gantt/ganttDetector.js';
|
||||
import { info } from '../diagrams/info/infoDetector.js';
|
||||
import pie from '../diagrams/pie/pieDetector.js';
|
||||
import { pie } from '../diagrams/pie/pieDetector.js';
|
||||
import quadrantChart from '../diagrams/quadrant-chart/quadrantDetector.js';
|
||||
import xychart from '../diagrams/xychart/xychartDetector.js';
|
||||
import requirement from '../diagrams/requirement/requirementDetector.js';
|
||||
import sequence from '../diagrams/sequence/sequenceDetector.js';
|
||||
import classDiagram from '../diagrams/class/classDetector.js';
|
||||
@@ -43,7 +44,11 @@ export const addDiagrams = () => {
|
||||
},
|
||||
},
|
||||
styles: {}, // should never be used
|
||||
renderer: {}, // should never be used
|
||||
renderer: {
|
||||
draw: () => {
|
||||
// should never be used
|
||||
},
|
||||
},
|
||||
parser: {
|
||||
parser: { yy: {} },
|
||||
parse: () => {
|
||||
@@ -81,6 +86,7 @@ export const addDiagrams = () => {
|
||||
state,
|
||||
journey,
|
||||
quadrantChart,
|
||||
sankey
|
||||
sankey,
|
||||
xychart
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { detectType } from './detectType.js';
|
||||
import { getDiagram, registerDiagram } from './diagramAPI.js';
|
||||
import { addDiagrams } from './diagram-orchestration.js';
|
||||
import { DiagramDetector } from './types.js';
|
||||
import type { DiagramDetector } from './types.js';
|
||||
import { getDiagramFromText } from '../Diagram.js';
|
||||
import { it, describe, expect, beforeAll } from 'vitest';
|
||||
|
||||
@@ -35,8 +35,17 @@ describe('DiagramAPI', () => {
|
||||
'loki',
|
||||
{
|
||||
db: {},
|
||||
parser: {},
|
||||
renderer: {},
|
||||
parser: {
|
||||
parse: (_text) => {
|
||||
return;
|
||||
},
|
||||
parser: { yy: {} },
|
||||
},
|
||||
renderer: {
|
||||
draw: () => {
|
||||
// no-op
|
||||
},
|
||||
},
|
||||
styles: {},
|
||||
},
|
||||
detector
|
||||
|
||||
@@ -4,9 +4,8 @@ import { getConfig as _getConfig } from '../config.js';
|
||||
import { sanitizeText as _sanitizeText } from '../diagrams/common/common.js';
|
||||
import { setupGraphViewbox as _setupGraphViewbox } from '../setupGraphViewbox.js';
|
||||
import { addStylesForDiagram } from '../styles.js';
|
||||
import { DiagramDefinition, DiagramDetector } from './types.js';
|
||||
import * as _commonDb from '../commonDb.js';
|
||||
import { parseDirective as _parseDirective } from '../directiveUtils.js';
|
||||
import type { DiagramDefinition, DiagramDetector } from './types.js';
|
||||
import * as _commonDb from '../diagrams/common/commonDb.js';
|
||||
|
||||
/*
|
||||
Packaging and exposing resources for external diagrams so that they can import
|
||||
@@ -21,8 +20,6 @@ export const setupGraphViewbox = _setupGraphViewbox;
|
||||
export const getCommonDb = () => {
|
||||
return _commonDb;
|
||||
};
|
||||
export const parseDirective = (p: any, statement: string, context: string, type: string) =>
|
||||
_parseDirective(p, statement, context, type);
|
||||
|
||||
const diagrams: Record<string, DiagramDefinition> = {};
|
||||
export interface Detectors {
|
||||
@@ -52,28 +49,29 @@ export const registerDiagram = (
|
||||
}
|
||||
addStylesForDiagram(id, diagram.styles);
|
||||
|
||||
if (diagram.injectUtils) {
|
||||
diagram.injectUtils(
|
||||
log,
|
||||
setLogLevel,
|
||||
getConfig,
|
||||
sanitizeText,
|
||||
setupGraphViewbox,
|
||||
getCommonDb(),
|
||||
parseDirective
|
||||
);
|
||||
}
|
||||
diagram.injectUtils?.(
|
||||
log,
|
||||
setLogLevel,
|
||||
getConfig,
|
||||
sanitizeText,
|
||||
setupGraphViewbox,
|
||||
getCommonDb(),
|
||||
() => {
|
||||
// parseDirective is removed in https://github.com/mermaid-js/mermaid/pull/4759.
|
||||
// This is a no-op for legacy support.
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const getDiagram = (name: string): DiagramDefinition => {
|
||||
if (name in diagrams) {
|
||||
return diagrams[name];
|
||||
}
|
||||
throw new Error(`Diagram ${name} not found.`);
|
||||
throw new DiagramNotFoundError(name);
|
||||
};
|
||||
|
||||
export class DiagramNotFoundError extends Error {
|
||||
constructor(message: string) {
|
||||
super(`Diagram ${message} not found.`);
|
||||
constructor(name: string) {
|
||||
super(`Diagram ${name} not found.`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,78 +1,171 @@
|
||||
import { vi } from 'vitest';
|
||||
import { extractFrontMatter } from './frontmatter.js';
|
||||
|
||||
const dbMock = () => ({ setDiagramTitle: vi.fn() });
|
||||
|
||||
describe('extractFrontmatter', () => {
|
||||
it('returns text unchanged if no frontmatter', () => {
|
||||
expect(extractFrontMatter('diagram', dbMock())).toEqual('diagram');
|
||||
expect(extractFrontMatter('diagram')).toMatchInlineSnapshot(`
|
||||
{
|
||||
"metadata": {},
|
||||
"text": "diagram",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('returns text unchanged if frontmatter lacks closing delimiter', () => {
|
||||
const text = `---\ntitle: foo\ndiagram`;
|
||||
expect(extractFrontMatter(text, dbMock())).toEqual(text);
|
||||
expect(extractFrontMatter(text)).toMatchInlineSnapshot(`
|
||||
{
|
||||
"metadata": {},
|
||||
"text": "---
|
||||
title: foo
|
||||
diagram",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('handles empty frontmatter', () => {
|
||||
const db = dbMock();
|
||||
const text = `---\n\n---\ndiagram`;
|
||||
expect(extractFrontMatter(text, db)).toEqual('diagram');
|
||||
expect(db.setDiagramTitle).not.toHaveBeenCalled();
|
||||
expect(extractFrontMatter(text)).toMatchInlineSnapshot(`
|
||||
{
|
||||
"metadata": {},
|
||||
"text": "diagram",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('handles frontmatter without mappings', () => {
|
||||
const db = dbMock();
|
||||
const text = `---\n1\n---\ndiagram`;
|
||||
expect(extractFrontMatter(text, db)).toEqual('diagram');
|
||||
expect(db.setDiagramTitle).not.toHaveBeenCalled();
|
||||
expect(extractFrontMatter(`---\n1\n---\ndiagram`)).toMatchInlineSnapshot(`
|
||||
{
|
||||
"metadata": {},
|
||||
"text": "diagram",
|
||||
}
|
||||
`);
|
||||
expect(extractFrontMatter(`---\n-1\n-2\n---\ndiagram`)).toMatchInlineSnapshot(`
|
||||
{
|
||||
"metadata": {},
|
||||
"text": "diagram",
|
||||
}
|
||||
`);
|
||||
expect(extractFrontMatter(`---\nnull\n---\ndiagram`)).toMatchInlineSnapshot(`
|
||||
{
|
||||
"metadata": {},
|
||||
"text": "diagram",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('does not try to parse frontmatter at the end', () => {
|
||||
const db = dbMock();
|
||||
const text = `diagram\n---\ntitle: foo\n---\n`;
|
||||
expect(extractFrontMatter(text, db)).toEqual(text);
|
||||
expect(db.setDiagramTitle).not.toHaveBeenCalled();
|
||||
expect(extractFrontMatter(text)).toMatchInlineSnapshot(`
|
||||
{
|
||||
"metadata": {},
|
||||
"text": "diagram
|
||||
---
|
||||
title: foo
|
||||
---
|
||||
",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('handles frontmatter with multiple delimiters', () => {
|
||||
const db = dbMock();
|
||||
const text = `---\ntitle: foo---bar\n---\ndiagram\n---\ntest`;
|
||||
expect(extractFrontMatter(text, db)).toEqual('diagram\n---\ntest');
|
||||
expect(db.setDiagramTitle).toHaveBeenCalledWith('foo---bar');
|
||||
expect(extractFrontMatter(text)).toMatchInlineSnapshot(`
|
||||
{
|
||||
"metadata": {
|
||||
"title": "foo---bar",
|
||||
},
|
||||
"text": "diagram
|
||||
---
|
||||
test",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('handles frontmatter with multi-line string and multiple delimiters', () => {
|
||||
const db = dbMock();
|
||||
const text = `---\ntitle: |\n multi-line string\n ---\n---\ndiagram`;
|
||||
expect(extractFrontMatter(text, db)).toEqual('diagram');
|
||||
expect(db.setDiagramTitle).toHaveBeenCalledWith('multi-line string\n---\n');
|
||||
expect(extractFrontMatter(text)).toMatchInlineSnapshot(`
|
||||
{
|
||||
"metadata": {
|
||||
"title": "multi-line string
|
||||
---
|
||||
",
|
||||
},
|
||||
"text": "diagram",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('handles frontmatter with title', () => {
|
||||
const db = dbMock();
|
||||
const text = `---\ntitle: foo\n---\ndiagram`;
|
||||
expect(extractFrontMatter(text, db)).toEqual('diagram');
|
||||
expect(db.setDiagramTitle).toHaveBeenCalledWith('foo');
|
||||
expect(extractFrontMatter(text)).toMatchInlineSnapshot(`
|
||||
{
|
||||
"metadata": {
|
||||
"title": "foo",
|
||||
},
|
||||
"text": "diagram",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('handles booleans in frontmatter properly', () => {
|
||||
const db = dbMock();
|
||||
const text = `---\ntitle: true\n---\ndiagram`;
|
||||
expect(extractFrontMatter(text, db)).toEqual('diagram');
|
||||
expect(db.setDiagramTitle).toHaveBeenCalledWith('true');
|
||||
expect(extractFrontMatter(text)).toMatchInlineSnapshot(`
|
||||
{
|
||||
"metadata": {
|
||||
"title": "true",
|
||||
},
|
||||
"text": "diagram",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('ignores unspecified frontmatter keys', () => {
|
||||
const db = dbMock();
|
||||
const text = `---\ninvalid: true\ntitle: foo\ntest: bar\n---\ndiagram`;
|
||||
expect(extractFrontMatter(text, db)).toEqual('diagram');
|
||||
expect(db.setDiagramTitle).toHaveBeenCalledWith('foo');
|
||||
expect(extractFrontMatter(text)).toMatchInlineSnapshot(`
|
||||
{
|
||||
"metadata": {
|
||||
"title": "foo",
|
||||
},
|
||||
"text": "diagram",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('throws exception for invalid YAML syntax', () => {
|
||||
const text = `---\n!!!\n---\ndiagram`;
|
||||
expect(() => extractFrontMatter(text, dbMock())).toThrow(
|
||||
'tag suffix cannot contain exclamation marks'
|
||||
);
|
||||
expect(() => extractFrontMatter(text)).toThrow('tag suffix cannot contain exclamation marks');
|
||||
});
|
||||
|
||||
it('handles frontmatter with config', () => {
|
||||
const text = `---
|
||||
title: hello
|
||||
config:
|
||||
graph:
|
||||
string: hello
|
||||
number: 14
|
||||
boolean: false
|
||||
array: [1, 2, 3]
|
||||
---
|
||||
diagram`;
|
||||
expect(extractFrontMatter(text)).toMatchInlineSnapshot(`
|
||||
{
|
||||
"metadata": {
|
||||
"config": {
|
||||
"graph": {
|
||||
"array": [
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
],
|
||||
"boolean": false,
|
||||
"number": 14,
|
||||
"string": "hello",
|
||||
},
|
||||
},
|
||||
"title": "hello",
|
||||
},
|
||||
"text": "diagram",
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,46 +1,60 @@
|
||||
import { DiagramDB } from './types.js';
|
||||
import type { MermaidConfig } from '../config.type.js';
|
||||
import { frontMatterRegex } from './regexes.js';
|
||||
// The "* as yaml" part is necessary for tree-shaking
|
||||
import * as yaml from 'js-yaml';
|
||||
|
||||
// Match Jekyll-style front matter blocks (https://jekyllrb.com/docs/front-matter/).
|
||||
// Based on regex used by Jekyll: https://github.com/jekyll/jekyll/blob/6dd3cc21c40b98054851846425af06c64f9fb466/lib/jekyll/document.rb#L10
|
||||
// Note that JS doesn't support the "\A" anchor, which means we can't use
|
||||
// multiline mode.
|
||||
// Relevant YAML spec: https://yaml.org/spec/1.2.2/#914-explicit-documents
|
||||
export const frontMatterRegex = /^-{3}\s*[\n\r](.*?)[\n\r]-{3}\s*[\n\r]+/s;
|
||||
|
||||
type FrontMatterMetadata = {
|
||||
interface FrontMatterMetadata {
|
||||
title?: string;
|
||||
// Allows custom display modes. Currently used for compact mode in gantt charts.
|
||||
displayMode?: string;
|
||||
};
|
||||
config?: MermaidConfig;
|
||||
}
|
||||
|
||||
export interface FrontMatterResult {
|
||||
text: string;
|
||||
metadata: FrontMatterMetadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract and parse frontmatter from text, if present, and sets appropriate
|
||||
* properties in the provided db.
|
||||
* @param text - The text that may have a YAML frontmatter.
|
||||
* @param db - Diagram database, could be of any diagram.
|
||||
* @returns text with frontmatter stripped out
|
||||
*/
|
||||
export function extractFrontMatter(text: string, db: DiagramDB): string {
|
||||
export function extractFrontMatter(text: string): FrontMatterResult {
|
||||
const matches = text.match(frontMatterRegex);
|
||||
if (matches) {
|
||||
const parsed: FrontMatterMetadata = yaml.load(matches[1], {
|
||||
// To keep things simple, only allow strings, arrays, and plain objects.
|
||||
// https://www.yaml.org/spec/1.2/spec.html#id2802346
|
||||
schema: yaml.FAILSAFE_SCHEMA,
|
||||
}) as FrontMatterMetadata;
|
||||
|
||||
if (parsed?.title) {
|
||||
db.setDiagramTitle?.(parsed.title);
|
||||
}
|
||||
|
||||
if (parsed?.displayMode) {
|
||||
db.setDisplayMode?.(parsed.displayMode);
|
||||
}
|
||||
|
||||
return text.slice(matches[0].length);
|
||||
} else {
|
||||
return text;
|
||||
if (!matches) {
|
||||
return {
|
||||
text,
|
||||
metadata: {},
|
||||
};
|
||||
}
|
||||
|
||||
let parsed: FrontMatterMetadata =
|
||||
yaml.load(matches[1], {
|
||||
// To support config, we need JSON schema.
|
||||
// https://www.yaml.org/spec/1.2/spec.html#id2803231
|
||||
schema: yaml.JSON_SCHEMA,
|
||||
}) ?? {};
|
||||
|
||||
// To handle runtime data type changes
|
||||
parsed = typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
|
||||
|
||||
const metadata: FrontMatterMetadata = {};
|
||||
|
||||
// Only add properties that are explicitly supported, if they exist
|
||||
if (parsed.displayMode) {
|
||||
metadata.displayMode = parsed.displayMode.toString();
|
||||
}
|
||||
if (parsed.title) {
|
||||
metadata.title = parsed.title.toString();
|
||||
}
|
||||
if (parsed.config) {
|
||||
metadata.config = parsed.config;
|
||||
}
|
||||
|
||||
return {
|
||||
text: text.slice(matches[0].length),
|
||||
metadata,
|
||||
};
|
||||
}
|
||||
|
||||
36
packages/mermaid/src/diagram-api/loadDiagram.ts
Normal file
36
packages/mermaid/src/diagram-api/loadDiagram.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { log } from '../logger.js';
|
||||
import { detectors } from './detectType.js';
|
||||
import { getDiagram, registerDiagram } from './diagramAPI.js';
|
||||
|
||||
export const loadRegisteredDiagrams = async () => {
|
||||
log.debug(`Loading registered diagrams`);
|
||||
// Load all lazy loaded diagrams in parallel
|
||||
const results = await Promise.allSettled(
|
||||
Object.entries(detectors).map(async ([key, { detector, loader }]) => {
|
||||
if (loader) {
|
||||
try {
|
||||
getDiagram(key);
|
||||
} catch (error) {
|
||||
try {
|
||||
// Register diagram if it is not already registered
|
||||
const { diagram, id } = await loader();
|
||||
registerDiagram(id, diagram, detector);
|
||||
} catch (err) {
|
||||
// Remove failed diagram from detectors
|
||||
log.error(`Failed to load external diagram with key ${key}. Removing from detectors.`);
|
||||
delete detectors[key];
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
const failed = results.filter((result) => result.status === 'rejected');
|
||||
if (failed.length > 0) {
|
||||
log.error(`Failed to load ${failed.length} external diagrams`);
|
||||
for (const res of failed) {
|
||||
log.error(res);
|
||||
}
|
||||
throw new Error(`Failed to load ${failed.length} external diagrams`);
|
||||
}
|
||||
};
|
||||
11
packages/mermaid/src/diagram-api/regexes.ts
Normal file
11
packages/mermaid/src/diagram-api/regexes.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
// Match Jekyll-style front matter blocks (https://jekyllrb.com/docs/front-matter/).
|
||||
// Based on regex used by Jekyll: https://github.com/jekyll/jekyll/blob/6dd3cc21c40b98054851846425af06c64f9fb466/lib/jekyll/document.rb#L10
|
||||
// Note that JS doesn't support the "\A" anchor, which means we can't use
|
||||
// multiline mode.
|
||||
// Relevant YAML spec: https://yaml.org/spec/1.2.2/#914-explicit-documents
|
||||
export const frontMatterRegex = /^-{3}\s*[\n\r](.*?)[\n\r]-{3}\s*[\n\r]+/s;
|
||||
|
||||
export const directiveRegex =
|
||||
/%{2}{\s*(?:(\w+)\s*:|(\w+))\s*(?:(\w+)|((?:(?!}%{2}).|\r?\n)*))?\s*(?:}%{2})?/gi;
|
||||
|
||||
export const anyCommentRegex = /\s*%%.*\n/gm;
|
||||
@@ -1,7 +1,13 @@
|
||||
import { Diagram } from '../Diagram.js';
|
||||
import { MermaidConfig } from '../config.type.js';
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import type { Diagram } from '../Diagram.js';
|
||||
import type { BaseDiagramConfig, MermaidConfig } from '../config.type.js';
|
||||
import type * as d3 from 'd3';
|
||||
|
||||
export interface DiagramMetadata {
|
||||
title?: string;
|
||||
config?: MermaidConfig;
|
||||
}
|
||||
|
||||
export interface InjectUtils {
|
||||
_log: any;
|
||||
_setLogLevel: any;
|
||||
@@ -9,6 +15,7 @@ export interface InjectUtils {
|
||||
_sanitizeText: any;
|
||||
_setupGraphViewbox: any;
|
||||
_commonDb: any;
|
||||
/** @deprecated as directives will be pre-processed since https://github.com/mermaid-js/mermaid/pull/4759 */
|
||||
_parseDirective: any;
|
||||
}
|
||||
|
||||
@@ -16,18 +23,43 @@ export interface InjectUtils {
|
||||
* Generic Diagram DB that may apply to any diagram type.
|
||||
*/
|
||||
export interface DiagramDB {
|
||||
// config
|
||||
getConfig?: () => BaseDiagramConfig | undefined;
|
||||
|
||||
// db
|
||||
clear?: () => void;
|
||||
setDiagramTitle?: (title: string) => void;
|
||||
setDisplayMode?: (title: string) => void;
|
||||
getDiagramTitle?: () => string;
|
||||
setAccTitle?: (title: string) => void;
|
||||
getAccTitle?: () => string;
|
||||
setAccDescription?: (describetion: string) => void;
|
||||
getAccDescription?: () => string;
|
||||
|
||||
setDisplayMode?: (title: string) => void;
|
||||
bindFunctions?: (element: Element) => void;
|
||||
}
|
||||
|
||||
// This is what is returned from getClasses(...) methods.
|
||||
// It is slightly renamed to ..StyleClassDef instead of just ClassDef because "class" is a greatly ambiguous and overloaded word.
|
||||
// It makes it clear we're working with a style class definition, even though defining the type is currently difficult.
|
||||
export interface DiagramStyleClassDef {
|
||||
id: string;
|
||||
styles?: string[];
|
||||
textStyles?: string[];
|
||||
}
|
||||
|
||||
export interface DiagramRenderer {
|
||||
draw: DrawDefinition;
|
||||
getClasses?: (
|
||||
text: string,
|
||||
diagram: Pick<DiagramDefinition, 'db'>
|
||||
) => Record<string, DiagramStyleClassDef>;
|
||||
}
|
||||
|
||||
export interface DiagramDefinition {
|
||||
db: DiagramDB;
|
||||
renderer: any;
|
||||
parser: any;
|
||||
renderer: DiagramRenderer;
|
||||
parser: ParserDefinition;
|
||||
styles?: any;
|
||||
init?: (config: MermaidConfig) => void;
|
||||
injectUtils?: (
|
||||
@@ -37,6 +69,7 @@ export interface DiagramDefinition {
|
||||
_sanitizeText: InjectUtils['_sanitizeText'],
|
||||
_setupGraphViewbox: InjectUtils['_setupGraphViewbox'],
|
||||
_commonDb: InjectUtils['_commonDb'],
|
||||
/** @deprecated as directives will be pre-processed since https://github.com/mermaid-js/mermaid/pull/4759 */
|
||||
_parseDirective: InjectUtils['_parseDirective']
|
||||
) => void;
|
||||
}
|
||||
@@ -67,20 +100,19 @@ export type DrawDefinition = (
|
||||
text: string,
|
||||
id: string,
|
||||
version: string,
|
||||
diagramObject: Diagram
|
||||
) => void;
|
||||
diagramObject: Diagram,
|
||||
error: Error | null,
|
||||
) => void | Promise<void>;
|
||||
|
||||
/**
|
||||
* Type for function parse directive from diagram code.
|
||||
*
|
||||
* @param statement -
|
||||
* @param context -
|
||||
* @param type -
|
||||
*/
|
||||
export type ParseDirectiveDefinition = (statement: string, context: string, type: string) => void;
|
||||
export interface ParserDefinition {
|
||||
parse: (text: string) => void;
|
||||
parser: { yy: DiagramDB };
|
||||
}
|
||||
|
||||
export type HTML = d3.Selection<HTMLIFrameElement, unknown, Element, unknown>;
|
||||
export type HTML = d3.Selection<HTMLIFrameElement, unknown, Element | null, unknown>;
|
||||
|
||||
export type SVG = d3.Selection<SVGSVGElement, unknown, Element, unknown>;
|
||||
export type SVG = d3.Selection<SVGSVGElement, unknown, Element | null, unknown>;
|
||||
|
||||
export type Group = d3.Selection<SVGGElement, unknown, Element | null, unknown>;
|
||||
|
||||
export type DiagramStylesProvider = (options?: any) => string;
|
||||
|
||||
@@ -34,7 +34,11 @@ describe('diagram detection', () => {
|
||||
yy: {},
|
||||
},
|
||||
},
|
||||
renderer: {},
|
||||
renderer: {
|
||||
draw: () => {
|
||||
// no-op
|
||||
},
|
||||
},
|
||||
styles: {},
|
||||
},
|
||||
})
|
||||
@@ -49,7 +53,7 @@ describe('diagram detection', () => {
|
||||
"Parse error on line 2:
|
||||
graph TD; A-->
|
||||
--------------^
|
||||
Expecting 'AMP', 'ALPHA', 'COLON', 'PIPE', 'TESTSTR', 'DOWN', 'DEFAULT', 'NUM', 'COMMA', 'MINUS', 'BRKT', 'DOT', 'PUNCTUATION', 'UNICODE_TEXT', 'PLUS', 'EQUALS', 'MULT', 'UNDERSCORE', got 'EOF'"
|
||||
Expecting 'AMP', 'COLON', 'PIPE', 'TESTSTR', 'DOWN', 'DEFAULT', 'NUM', 'COMMA', 'NODE_STRING', 'BRKT', 'MINUS', 'MULT', 'UNICODE_TEXT', got 'EOF'"
|
||||
`);
|
||||
await expect(getDiagramFromText('sequenceDiagram; A-->B')).rejects
|
||||
.toThrowErrorMatchingInlineSnapshot(`
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import mermaidAPI from '../../mermaidAPI.js';
|
||||
import * as configApi from '../../config.js';
|
||||
import { sanitizeText } from '../common/common.js';
|
||||
import { setAccTitle, getAccTitle, getAccDescription, setAccDescription } from '../../commonDb.js';
|
||||
import {
|
||||
setAccTitle,
|
||||
getAccTitle,
|
||||
getAccDescription,
|
||||
setAccDescription,
|
||||
} from '../common/commonDb.js';
|
||||
|
||||
let c4ShapeArray = [];
|
||||
let boundaryParseStack = [''];
|
||||
@@ -33,10 +37,6 @@ export const setC4Type = function (c4TypeParam) {
|
||||
c4Type = sanitizedText;
|
||||
};
|
||||
|
||||
export const parseDirective = function (statement, context, type) {
|
||||
mermaidAPI.parseDirective(this, statement, context, type);
|
||||
};
|
||||
|
||||
//type, from, to, label, ?techn, ?descr, ?sprite, ?tags, $link
|
||||
export const addRel = function (type, from, to, label, techn, descr, sprite, tags, link) {
|
||||
// Don't allow label nulling
|
||||
@@ -816,7 +816,6 @@ export default {
|
||||
getAccTitle,
|
||||
getAccDescription,
|
||||
setAccDescription,
|
||||
parseDirective,
|
||||
getConfig: () => configApi.getConfig().c4,
|
||||
clear,
|
||||
LINETYPE,
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
// @ts-ignore: TODO Fix ts errors
|
||||
import c4Parser from './parser/c4Diagram.jison';
|
||||
import c4Db from './c4Db.js';
|
||||
import c4Renderer from './c4Renderer.js';
|
||||
import c4Styles from './styles.js';
|
||||
import { MermaidConfig } from '../../config.type.js';
|
||||
import { DiagramDefinition } from '../../diagram-api/types.js';
|
||||
// @ts-ignore: JISON doesn't support types
|
||||
import parser from './parser/c4Diagram.jison';
|
||||
import db from './c4Db.js';
|
||||
import renderer from './c4Renderer.js';
|
||||
import styles from './styles.js';
|
||||
import type { MermaidConfig } from '../../config.type.js';
|
||||
import type { DiagramDefinition } from '../../diagram-api/types.js';
|
||||
|
||||
export const diagram: DiagramDefinition = {
|
||||
parser: c4Parser,
|
||||
db: c4Db,
|
||||
renderer: c4Renderer,
|
||||
styles: c4Styles,
|
||||
init: (cnf: MermaidConfig) => {
|
||||
c4Renderer.setConf(cnf.c4);
|
||||
parser,
|
||||
db,
|
||||
renderer,
|
||||
styles,
|
||||
init: ({ c4, wrap }: MermaidConfig) => {
|
||||
renderer.setConf(c4);
|
||||
db.setWrap(wrap);
|
||||
},
|
||||
};
|
||||
|
||||
135
packages/mermaid/src/diagrams/c4/parser/c4Container.spec.js
Normal file
135
packages/mermaid/src/diagrams/c4/parser/c4Container.spec.js
Normal file
@@ -0,0 +1,135 @@
|
||||
import c4Db from '../c4Db.js';
|
||||
import c4 from './c4Diagram.jison';
|
||||
import { setConfig } from '../../../config.js';
|
||||
|
||||
setConfig({
|
||||
securityLevel: 'strict',
|
||||
});
|
||||
|
||||
describe.each([
|
||||
['Container', 'container'],
|
||||
['ContainerDb', 'container_db'],
|
||||
['ContainerQueue', 'container_queue'],
|
||||
['Container_Ext', 'external_container'],
|
||||
['ContainerDb_Ext', 'external_container_db'],
|
||||
['ContainerQueue_Ext', 'external_container_queue'],
|
||||
])('parsing a C4 %s', function (macroName, elementName) {
|
||||
beforeEach(function () {
|
||||
c4.parser.yy = c4Db;
|
||||
c4.parser.yy.clear();
|
||||
});
|
||||
|
||||
it('should parse a C4 diagram with one Container correctly', function () {
|
||||
c4.parser.parse(`C4Context
|
||||
title Container diagram for Internet Banking Container
|
||||
${macroName}(ContainerAA, "Internet Banking Container", "Technology", "Allows customers to view information about their bank accounts, and make payments.")`);
|
||||
|
||||
const yy = c4.parser.yy;
|
||||
|
||||
const shapes = yy.getC4ShapeArray();
|
||||
expect(shapes.length).toBe(1);
|
||||
const onlyShape = shapes[0];
|
||||
|
||||
expect(onlyShape).toEqual({
|
||||
alias: 'ContainerAA',
|
||||
descr: {
|
||||
text: 'Allows customers to view information about their bank accounts, and make payments.',
|
||||
},
|
||||
label: {
|
||||
text: 'Internet Banking Container',
|
||||
},
|
||||
link: undefined,
|
||||
sprite: undefined,
|
||||
tags: undefined,
|
||||
parentBoundary: 'global',
|
||||
typeC4Shape: {
|
||||
text: elementName,
|
||||
},
|
||||
techn: {
|
||||
text: 'Technology',
|
||||
},
|
||||
wrap: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('should parse the alias', function () {
|
||||
c4.parser.parse(`C4Context
|
||||
${macroName}(ContainerAA, "Internet Banking Container")`);
|
||||
|
||||
expect(c4.parser.yy.getC4ShapeArray()[0]).toMatchObject({
|
||||
alias: 'ContainerAA',
|
||||
});
|
||||
});
|
||||
|
||||
it('should parse the label', function () {
|
||||
c4.parser.parse(`C4Context
|
||||
${macroName}(ContainerAA, "Internet Banking Container")`);
|
||||
|
||||
expect(c4.parser.yy.getC4ShapeArray()[0]).toMatchObject({
|
||||
label: {
|
||||
text: 'Internet Banking Container',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should parse the technology', function () {
|
||||
c4.parser.parse(`C4Context
|
||||
${macroName}(ContainerAA, "", "Java")`);
|
||||
|
||||
expect(c4.parser.yy.getC4ShapeArray()[0]).toMatchObject({
|
||||
techn: {
|
||||
text: 'Java',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should parse the description', function () {
|
||||
c4.parser.parse(`C4Context
|
||||
${macroName}(ContainerAA, "", "", "Allows customers to view information about their bank accounts, and make payments.")`);
|
||||
|
||||
expect(c4.parser.yy.getC4ShapeArray()[0]).toMatchObject({
|
||||
descr: {
|
||||
text: 'Allows customers to view information about their bank accounts, and make payments.',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should parse a sprite', function () {
|
||||
c4.parser.parse(`C4Context
|
||||
${macroName}(ContainerAA, $sprite="users")`);
|
||||
|
||||
expect(c4.parser.yy.getC4ShapeArray()[0]).toMatchObject({
|
||||
label: {
|
||||
text: {
|
||||
sprite: 'users',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should parse a link', function () {
|
||||
c4.parser.parse(`C4Context
|
||||
${macroName}(ContainerAA, $link="https://github.com/mermaidjs")`);
|
||||
|
||||
expect(c4.parser.yy.getC4ShapeArray()[0]).toMatchObject({
|
||||
label: {
|
||||
text: {
|
||||
link: 'https://github.com/mermaidjs',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should parse tags', function () {
|
||||
c4.parser.parse(`C4Context
|
||||
${macroName}(ContainerAA, $tags="tag1,tag2")`);
|
||||
|
||||
expect(c4.parser.yy.getC4ShapeArray()[0]).toMatchObject({
|
||||
label: {
|
||||
text: {
|
||||
tags: 'tag1,tag2',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -72,25 +72,16 @@
|
||||
%x string_kv_key
|
||||
%x string_kv_value
|
||||
|
||||
%x open_directive
|
||||
%x type_directive
|
||||
%x arg_directive
|
||||
%x close_directive
|
||||
%x acc_title
|
||||
%x acc_descr
|
||||
%x acc_descr_multiline
|
||||
|
||||
%%
|
||||
|
||||
\%\%\{ { this.begin('open_directive'); return 'open_directive'; }
|
||||
.*direction\s+TB[^\n]* return 'direction_tb';
|
||||
.*direction\s+BT[^\n]* return 'direction_bt';
|
||||
.*direction\s+RL[^\n]* return 'direction_rl';
|
||||
.*direction\s+LR[^\n]* return 'direction_lr';
|
||||
<open_directive>((?:(?!\}\%\%)[^:.])*) { this.begin('type_directive'); return 'type_directive'; }
|
||||
<type_directive>":" { this.popState(); this.begin('arg_directive'); return ':'; }
|
||||
<type_directive,arg_directive>\}\%\% { this.popState(); this.popState(); return 'close_directive'; }
|
||||
<arg_directive>((?:(?!\}\%\%).|\n)*) return 'arg_directive';
|
||||
|
||||
|
||||
"title"\s[^#\n;]+ return 'title';
|
||||
@@ -150,27 +141,27 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multiline");}
|
||||
"Node_R" { this.begin("node_r"); return 'NODE_R';}
|
||||
|
||||
|
||||
"Rel" { this.begin("rel"); return 'REL';}
|
||||
"BiRel" { this.begin("birel"); return 'BIREL';}
|
||||
"Rel_Up" { this.begin("rel_u"); return 'REL_U';}
|
||||
"Rel_U" { this.begin("rel_u"); return 'REL_U';}
|
||||
"Rel_Down" { this.begin("rel_d"); return 'REL_D';}
|
||||
"Rel_D" { this.begin("rel_d"); return 'REL_D';}
|
||||
"Rel_Left" { this.begin("rel_l"); return 'REL_L';}
|
||||
"Rel_L" { this.begin("rel_l"); return 'REL_L';}
|
||||
"Rel_Right" { this.begin("rel_r"); return 'REL_R';}
|
||||
"Rel_R" { this.begin("rel_r"); return 'REL_R';}
|
||||
"Rel_Back" { this.begin("rel_b"); return 'REL_B';}
|
||||
"RelIndex" { this.begin("rel_index"); return 'REL_INDEX';}
|
||||
"Rel" { this.begin("rel"); return 'REL';}
|
||||
"BiRel" { this.begin("birel"); return 'BIREL';}
|
||||
"Rel_Up" { this.begin("rel_u"); return 'REL_U';}
|
||||
"Rel_U" { this.begin("rel_u"); return 'REL_U';}
|
||||
"Rel_Down" { this.begin("rel_d"); return 'REL_D';}
|
||||
"Rel_D" { this.begin("rel_d"); return 'REL_D';}
|
||||
"Rel_Left" { this.begin("rel_l"); return 'REL_L';}
|
||||
"Rel_L" { this.begin("rel_l"); return 'REL_L';}
|
||||
"Rel_Right" { this.begin("rel_r"); return 'REL_R';}
|
||||
"Rel_R" { this.begin("rel_r"); return 'REL_R';}
|
||||
"Rel_Back" { this.begin("rel_b"); return 'REL_B';}
|
||||
"RelIndex" { this.begin("rel_index"); return 'REL_INDEX';}
|
||||
|
||||
"UpdateElementStyle" { this.begin("update_el_style"); return 'UPDATE_EL_STYLE';}
|
||||
"UpdateRelStyle" { this.begin("update_rel_style"); return 'UPDATE_REL_STYLE';}
|
||||
"UpdateLayoutConfig" { this.begin("update_layout_config"); return 'UPDATE_LAYOUT_CONFIG';}
|
||||
"UpdateElementStyle" { this.begin("update_el_style"); return 'UPDATE_EL_STYLE';}
|
||||
"UpdateRelStyle" { this.begin("update_rel_style"); return 'UPDATE_REL_STYLE';}
|
||||
"UpdateLayoutConfig" { this.begin("update_layout_config"); return 'UPDATE_LAYOUT_CONFIG';}
|
||||
|
||||
<person,person_ext,system_ext_queue,system_ext_db,system_ext,system_queue,system_db,system,boundary,enterprise_boundary,system_boundary,container_ext_db,container_ext,container_queue,container_db,container,container_boundary,component_ext_db,component_ext,component_queue,component_db,component,node,node_l,node_r,rel,birel,rel_u,rel_d,rel_l,rel_r,rel_b,rel_index,update_el_style,update_rel_style,update_layout_config><<EOF>> return "EOF_IN_STRUCT";
|
||||
<person,person_ext,system_ext_queue,system_ext_db,system_ext,system_queue,system_db,system,boundary,enterprise_boundary,system_boundary,container_ext_db,container_ext,container_queue,container_db,container,container_boundary,component_ext_db,component_ext,component_queue,component_db,component,node,node_l,node_r,rel,birel,rel_u,rel_d,rel_l,rel_r,rel_b,rel_index,update_el_style,update_rel_style,update_layout_config>[(][ ]*[,] { this.begin("attribute"); return "ATTRIBUTE_EMPTY";}
|
||||
<person,person_ext,system_ext_queue,system_ext_db,system_ext,system_queue,system_db,system,boundary,enterprise_boundary,system_boundary,container_ext_db,container_ext,container_queue,container_db,container,container_boundary,component_ext_db,component_ext,component_queue,component_db,component,node,node_l,node_r,rel,birel,rel_u,rel_d,rel_l,rel_r,rel_b,rel_index,update_el_style,update_rel_style,update_layout_config>[(] { this.begin("attribute"); }
|
||||
<person,person_ext,system_ext_queue,system_ext_db,system_ext,system_queue,system_db,system,boundary,enterprise_boundary,system_boundary,container_ext_db,container_ext,container_queue,container_db,container,container_boundary,component_ext_db,component_ext,component_queue,component_db,component,node,node_l,node_r,rel,birel,rel_u,rel_d,rel_l,rel_r,rel_b,rel_index,update_el_style,update_rel_style,update_layout_config,attribute>[)] { this.popState();this.popState();}
|
||||
<person,person_ext,system_ext_queue,system_ext_db,system_ext,system_queue,system_db,system,boundary,enterprise_boundary,system_boundary,container_ext_db,container_ext_queue,container_ext,container_queue,container_db,container,container_boundary,component_ext_db,component_ext,component_queue,component_db,component,node,node_l,node_r,rel,birel,rel_u,rel_d,rel_l,rel_r,rel_b,rel_index,update_el_style,update_rel_style,update_layout_config><<EOF>> return "EOF_IN_STRUCT";
|
||||
<person,person_ext,system_ext_queue,system_ext_db,system_ext,system_queue,system_db,system,boundary,enterprise_boundary,system_boundary,container_ext_db,container_ext_queue,container_ext,container_queue,container_db,container,container_boundary,component_ext_db,component_ext,component_queue,component_db,component,node,node_l,node_r,rel,birel,rel_u,rel_d,rel_l,rel_r,rel_b,rel_index,update_el_style,update_rel_style,update_layout_config>[(][ ]*[,] { this.begin("attribute"); return "ATTRIBUTE_EMPTY";}
|
||||
<person,person_ext,system_ext_queue,system_ext_db,system_ext,system_queue,system_db,system,boundary,enterprise_boundary,system_boundary,container_ext_db,container_ext_queue,container_ext,container_queue,container_db,container,container_boundary,component_ext_db,component_ext,component_queue,component_db,component,node,node_l,node_r,rel,birel,rel_u,rel_d,rel_l,rel_r,rel_b,rel_index,update_el_style,update_rel_style,update_layout_config>[(] { this.begin("attribute"); }
|
||||
<person,person_ext,system_ext_queue,system_ext_db,system_ext,system_queue,system_db,system,boundary,enterprise_boundary,system_boundary,container_ext_db,container_ext_queue,container_ext,container_queue,container_db,container,container_boundary,component_ext_db,component_ext,component_queue,component_db,component,node,node_l,node_r,rel,birel,rel_u,rel_d,rel_l,rel_r,rel_b,rel_index,update_el_style,update_rel_style,update_layout_config,attribute>[)] { this.popState();this.popState();}
|
||||
|
||||
<attribute>",," { return 'ATTRIBUTE_EMPTY';}
|
||||
<attribute>"," { }
|
||||
@@ -189,7 +180,7 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multiline");}
|
||||
|
||||
'{' { /* this.begin("lbrace"); */ return "LBRACE";}
|
||||
'}' { /* this.popState(); */ return "RBRACE";}
|
||||
|
||||
|
||||
[\s]+ return 'SPACE';
|
||||
[\n\r]+ return 'EOL';
|
||||
<<EOF>> return 'EOF';
|
||||
@@ -207,7 +198,6 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multiline");}
|
||||
start
|
||||
: mermaidDoc
|
||||
| direction
|
||||
| directive start
|
||||
;
|
||||
|
||||
direction
|
||||
@@ -225,26 +215,6 @@ mermaidDoc
|
||||
: graphConfig
|
||||
;
|
||||
|
||||
directive
|
||||
: openDirective typeDirective closeDirective NEWLINE
|
||||
| openDirective typeDirective ':' argDirective closeDirective NEWLINE
|
||||
;
|
||||
|
||||
openDirective
|
||||
: open_directive { yy.parseDirective('%%{', 'open_directive'); }
|
||||
;
|
||||
|
||||
typeDirective
|
||||
: type_directive { }
|
||||
;
|
||||
|
||||
argDirective
|
||||
: arg_directive { $1 = $1.trim().replace(/'/g, '"'); yy.parseDirective($1, 'arg_directive'); }
|
||||
;
|
||||
|
||||
closeDirective
|
||||
: close_directive { yy.parseDirective('}%%', 'close_directive', 'c4Context'); }
|
||||
;
|
||||
|
||||
graphConfig
|
||||
: C4_CONTEXT NEWLINE statements EOF {yy.setC4Type($1)}
|
||||
@@ -257,7 +227,7 @@ graphConfig
|
||||
statements
|
||||
: otherStatements
|
||||
| diagramStatements
|
||||
| otherStatements diagramStatements
|
||||
| otherStatements diagramStatements
|
||||
;
|
||||
|
||||
otherStatements
|
||||
@@ -268,10 +238,10 @@ otherStatements
|
||||
|
||||
otherStatement
|
||||
: title {yy.setTitle($1.substring(6));$$=$1.substring(6);}
|
||||
| accDescription {yy.setAccDescription($1.substring(15));$$=$1.substring(15);}
|
||||
| accDescription {yy.setAccDescription($1.substring(15));$$=$1.substring(15);}
|
||||
| acc_title acc_title_value { $$=$2.trim();yy.setTitle($$); }
|
||||
| acc_descr acc_descr_value { $$=$2.trim();yy.setAccDescription($$); }
|
||||
| acc_descr_multiline_value { $$=$1.trim();yy.setAccDescription($$); }
|
||||
| acc_descr_multiline_value { $$=$1.trim();yy.setAccDescription($$); }
|
||||
;
|
||||
|
||||
boundaryStatement
|
||||
@@ -301,7 +271,7 @@ boundaryStopStatement
|
||||
diagramStatements
|
||||
: diagramStatement
|
||||
| diagramStatement NEWLINE
|
||||
| diagramStatement NEWLINE statements
|
||||
| diagramStatement NEWLINE statements
|
||||
;
|
||||
|
||||
diagramStatement
|
||||
@@ -312,19 +282,19 @@ diagramStatement
|
||||
| SYSTEM_QUEUE attributes {yy.addPersonOrSystem('system_queue', ...$2); $$=$2;}
|
||||
| SYSTEM_EXT attributes {yy.addPersonOrSystem('external_system', ...$2); $$=$2;}
|
||||
| SYSTEM_EXT_DB attributes {yy.addPersonOrSystem('external_system_db', ...$2); $$=$2;}
|
||||
| SYSTEM_EXT_QUEUE attributes {yy.addPersonOrSystem('external_system_queue', ...$2); $$=$2;}
|
||||
| SYSTEM_EXT_QUEUE attributes {yy.addPersonOrSystem('external_system_queue', ...$2); $$=$2;}
|
||||
| CONTAINER attributes {yy.addContainer('container', ...$2); $$=$2;}
|
||||
| CONTAINER_DB attributes {yy.addContainer('container_db', ...$2); $$=$2;}
|
||||
| CONTAINER_QUEUE attributes {yy.addContainer('container_queue', ...$2); $$=$2;}
|
||||
| CONTAINER_EXT attributes {yy.addContainer('external_container', ...$2); $$=$2;}
|
||||
| CONTAINER_EXT_DB attributes {yy.addContainer('external_container_db', ...$2); $$=$2;}
|
||||
| CONTAINER_EXT_QUEUE attributes {yy.addContainer('external_container_queue', ...$2); $$=$2;}
|
||||
| CONTAINER_EXT_QUEUE attributes {yy.addContainer('external_container_queue', ...$2); $$=$2;}
|
||||
| COMPONENT attributes {yy.addComponent('component', ...$2); $$=$2;}
|
||||
| COMPONENT_DB attributes {yy.addComponent('component_db', ...$2); $$=$2;}
|
||||
| COMPONENT_QUEUE attributes {yy.addComponent('component_queue', ...$2); $$=$2;}
|
||||
| COMPONENT_EXT attributes {yy.addComponent('external_component', ...$2); $$=$2;}
|
||||
| COMPONENT_EXT_DB attributes {yy.addComponent('external_component_db', ...$2); $$=$2;}
|
||||
| COMPONENT_EXT_QUEUE attributes {yy.addComponent('external_component_queue', ...$2); $$=$2;}
|
||||
| COMPONENT_EXT_QUEUE attributes {yy.addComponent('external_component_queue', ...$2); $$=$2;}
|
||||
| boundaryStatement
|
||||
| REL attributes {yy.addRel('rel', ...$2); $$=$2;}
|
||||
| BIREL attributes {yy.addRel('birel', ...$2); $$=$2;}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import common from '../common/common.js';
|
||||
import * as svgDrawCommon from '../common/svgDrawCommon';
|
||||
import * as svgDrawCommon from '../common/svgDrawCommon.js';
|
||||
import { sanitizeUrl } from '@braintree/sanitize-url';
|
||||
|
||||
export const drawRect = function (elem, rectData) {
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
// @ts-nocheck - don't check until handle it
|
||||
import { select, Selection } from 'd3';
|
||||
import type { Selection } from 'd3';
|
||||
import { select } from 'd3';
|
||||
import { log } from '../../logger.js';
|
||||
import * as configApi from '../../config.js';
|
||||
import common from '../common/common.js';
|
||||
import utils from '../../utils.js';
|
||||
import mermaidAPI from '../../mermaidAPI.js';
|
||||
import {
|
||||
setAccTitle,
|
||||
getAccTitle,
|
||||
@@ -13,8 +12,9 @@ import {
|
||||
clear as commonClear,
|
||||
setDiagramTitle,
|
||||
getDiagramTitle,
|
||||
} from '../../commonDb.js';
|
||||
import {
|
||||
} from '../common/commonDb.js';
|
||||
import { ClassMember } from './classTypes.js';
|
||||
import type {
|
||||
ClassRelation,
|
||||
ClassNode,
|
||||
ClassNote,
|
||||
@@ -36,12 +36,8 @@ let functions: any[] = [];
|
||||
|
||||
const sanitizeText = (txt: string) => common.sanitizeText(txt, configApi.getConfig());
|
||||
|
||||
export const parseDirective = function (statement: string, context: string, type: string) {
|
||||
// @ts-ignore Don't wanna mess it up
|
||||
mermaidAPI.parseDirective(this, statement, context, type);
|
||||
};
|
||||
|
||||
const splitClassNameAndType = function (id: string) {
|
||||
const splitClassNameAndType = function (_id: string) {
|
||||
const id = common.sanitizeText(_id, configApi.getConfig());
|
||||
let genericType = '';
|
||||
let className = id;
|
||||
|
||||
@@ -54,7 +50,8 @@ const splitClassNameAndType = function (id: string) {
|
||||
return { className: className, type: genericType };
|
||||
};
|
||||
|
||||
export const setClassLabel = function (id: string, label: string) {
|
||||
export const setClassLabel = function (_id: string, label: string) {
|
||||
const id = common.sanitizeText(_id, configApi.getConfig());
|
||||
if (label) {
|
||||
label = sanitizeText(label);
|
||||
}
|
||||
@@ -69,22 +66,25 @@ export const setClassLabel = function (id: string, label: string) {
|
||||
* @param id - Id of the class to add
|
||||
* @public
|
||||
*/
|
||||
export const addClass = function (id: string) {
|
||||
const classId = splitClassNameAndType(id);
|
||||
export const addClass = function (_id: string) {
|
||||
const id = common.sanitizeText(_id, configApi.getConfig());
|
||||
const { className, type } = splitClassNameAndType(id);
|
||||
// Only add class if not exists
|
||||
if (classes[classId.className] !== undefined) {
|
||||
if (Object.hasOwn(classes, className)) {
|
||||
return;
|
||||
}
|
||||
|
||||
classes[classId.className] = {
|
||||
id: classId.className,
|
||||
type: classId.type,
|
||||
label: classId.className,
|
||||
// alert('Adding class: ' + className);
|
||||
const name = common.sanitizeText(className, configApi.getConfig());
|
||||
// alert('Adding class after: ' + name);
|
||||
classes[name] = {
|
||||
id: name,
|
||||
type: type,
|
||||
label: name,
|
||||
cssClasses: [],
|
||||
methods: [],
|
||||
members: [],
|
||||
annotations: [],
|
||||
domId: MERMAID_DOM_ID_PREFIX + classId.className + '-' + classCounter,
|
||||
domId: MERMAID_DOM_ID_PREFIX + name + '-' + classCounter,
|
||||
} as ClassNode;
|
||||
|
||||
classCounter++;
|
||||
@@ -96,7 +96,8 @@ export const addClass = function (id: string) {
|
||||
* @param id - class ID to lookup
|
||||
* @public
|
||||
*/
|
||||
export const lookUpDomId = function (id: string): string {
|
||||
export const lookUpDomId = function (_id: string): string {
|
||||
const id = common.sanitizeText(_id, configApi.getConfig());
|
||||
if (id in classes) {
|
||||
return classes[id].domId;
|
||||
}
|
||||
@@ -114,11 +115,11 @@ export const clear = function () {
|
||||
commonClear();
|
||||
};
|
||||
|
||||
export const getClass = function (id: string) {
|
||||
export const getClass = function (id: string): ClassNode {
|
||||
return classes[id];
|
||||
};
|
||||
|
||||
export const getClasses = function () {
|
||||
export const getClasses = function (): ClassMap {
|
||||
return classes;
|
||||
};
|
||||
|
||||
@@ -174,6 +175,8 @@ export const addAnnotation = function (className: string, annotation: string) {
|
||||
* @public
|
||||
*/
|
||||
export const addMember = function (className: string, member: string) {
|
||||
addClass(className);
|
||||
|
||||
const validatedClassName = splitClassNameAndType(className).className;
|
||||
const theClass = classes[validatedClassName];
|
||||
|
||||
@@ -186,9 +189,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(sanitizeText(memberString));
|
||||
theClass.methods.push(new ClassMember(memberString, 'method'));
|
||||
} else if (memberString) {
|
||||
theClass.members.push(sanitizeText(memberString));
|
||||
theClass.members.push(new ClassMember(memberString, 'attribute'));
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -255,6 +258,7 @@ 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.
|
||||
*
|
||||
@@ -298,7 +302,8 @@ export const setClickEvent = function (ids: string, functionName: string, functi
|
||||
setCssClass(ids, 'clickable');
|
||||
};
|
||||
|
||||
const setClickFunc = function (domId: string, functionName: string, functionArgs: string) {
|
||||
const setClickFunc = function (_domId: string, functionName: string, functionArgs: string) {
|
||||
const domId = common.sanitizeText(_domId, configApi.getConfig());
|
||||
const config = configApi.getConfig();
|
||||
if (config.securityLevel !== 'loose') {
|
||||
return;
|
||||
@@ -367,6 +372,7 @@ export const relationType = {
|
||||
const setupToolTips = function (element: Element) {
|
||||
let tooltipElem: Selection<HTMLDivElement, unknown, HTMLElement, unknown> =
|
||||
select('.mermaidTooltip');
|
||||
// @ts-expect-error - Incorrect types
|
||||
if ((tooltipElem._groups || tooltipElem)[0][0] === null) {
|
||||
tooltipElem = select('body').append('div').attr('class', 'mermaidTooltip').style('opacity', 0);
|
||||
}
|
||||
@@ -376,7 +382,6 @@ const setupToolTips = function (element: Element) {
|
||||
const nodes = svg.selectAll('g.node');
|
||||
nodes
|
||||
.on('mouseover', function () {
|
||||
// @ts-expect-error - select is not part of the d3 type definition
|
||||
const el = select(this);
|
||||
const title = el.attr('title');
|
||||
// Don't try to draw a tooltip if no data is provided
|
||||
@@ -386,6 +391,7 @@ const setupToolTips = function (element: Element) {
|
||||
// @ts-ignore - getBoundingClientRect is not part of the d3 type definition
|
||||
const rect = this.getBoundingClientRect();
|
||||
|
||||
// @ts-expect-error - Incorrect types
|
||||
tooltipElem.transition().duration(200).style('opacity', '.9');
|
||||
tooltipElem
|
||||
.text(el.attr('title'))
|
||||
@@ -395,8 +401,8 @@ const setupToolTips = function (element: Element) {
|
||||
el.classed('hover', true);
|
||||
})
|
||||
.on('mouseout', function () {
|
||||
// @ts-expect-error - Incorrect types
|
||||
tooltipElem.transition().duration(500).style('opacity', 0);
|
||||
// @ts-expect-error - select is not part of the d3 type definition
|
||||
const el = select(this);
|
||||
el.classed('hover', false);
|
||||
});
|
||||
@@ -455,7 +461,6 @@ export const addClassesToNamespace = function (id: string, classNames: string[])
|
||||
};
|
||||
|
||||
export default {
|
||||
parseDirective,
|
||||
setAccTitle,
|
||||
getAccTitle,
|
||||
getAccDescription,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { DiagramDefinition } from '../../diagram-api/types.js';
|
||||
// @ts-ignore: TODO Fix ts errors
|
||||
import type { DiagramDefinition } from '../../diagram-api/types.js';
|
||||
// @ts-ignore: JISON doesn't support types
|
||||
import parser from './parser/classDiagram.jison';
|
||||
import db from './classDb.js';
|
||||
import styles from './styles.js';
|
||||
|
||||
@@ -4,6 +4,9 @@ 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 () {
|
||||
@@ -127,7 +130,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]).toBe('member1');
|
||||
expect(c1.members[0].getDisplayDetails().displayText).toBe('member1');
|
||||
});
|
||||
|
||||
it('should parse a class with a text label, member and annotation', () => {
|
||||
@@ -142,7 +145,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]).toBe('int member1');
|
||||
expect(c1.members[0].getDisplayDetails().displayText).toBe('int member1');
|
||||
expect(c1.annotations.length).toBe(1);
|
||||
expect(c1.annotations[0]).toBe('interface');
|
||||
});
|
||||
@@ -168,7 +171,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]).toBe('int member1');
|
||||
expect(c1.members[0].getDisplayDetails().displayText).toBe('int member1');
|
||||
expect(c1.cssClasses[0]).toBe('styleClass');
|
||||
});
|
||||
|
||||
@@ -264,6 +267,181 @@ class C13["With Città foreign language"]
|
||||
const str = 'classDiagram\n' + 'note "test"\n';
|
||||
parser.parse(str);
|
||||
});
|
||||
|
||||
const keywords = [
|
||||
'direction',
|
||||
'classDiagram',
|
||||
'classDiagram-v2',
|
||||
'namespace',
|
||||
'{}',
|
||||
'{',
|
||||
'}',
|
||||
'()',
|
||||
'(',
|
||||
')',
|
||||
'[]',
|
||||
'[',
|
||||
']',
|
||||
'class',
|
||||
'\n',
|
||||
'cssClass',
|
||||
'callback',
|
||||
'link',
|
||||
'click',
|
||||
'note',
|
||||
'note for',
|
||||
'<<',
|
||||
'>>',
|
||||
'call ',
|
||||
'~',
|
||||
'~Generic~',
|
||||
'_self',
|
||||
'_blank',
|
||||
'_parent',
|
||||
'_top',
|
||||
'<|',
|
||||
'|>',
|
||||
'>',
|
||||
'<',
|
||||
'*',
|
||||
'o',
|
||||
'\\',
|
||||
'--',
|
||||
'..',
|
||||
'-->',
|
||||
'--|>',
|
||||
': label',
|
||||
':::',
|
||||
'.',
|
||||
'+',
|
||||
'alphaNum',
|
||||
'!',
|
||||
'0123',
|
||||
'function()',
|
||||
'function(arg1, arg2)',
|
||||
];
|
||||
|
||||
it.each(keywords)('should handle a note with %s in it', function (keyword: string) {
|
||||
const str = `classDiagram
|
||||
note "This is a keyword: ${keyword}. It truly is."
|
||||
`;
|
||||
parser.parse(str);
|
||||
expect(classDb.getNotes()[0].text).toEqual(`This is a keyword: ${keyword}. It truly is.`);
|
||||
});
|
||||
|
||||
it.each(keywords)(
|
||||
'should handle note with %s at beginning of string',
|
||||
function (keyword: string) {
|
||||
const str = `classDiagram
|
||||
note "${keyword}"`;
|
||||
|
||||
parser.parse(str);
|
||||
expect(classDb.getNotes()[0].text).toEqual(`${keyword}`);
|
||||
}
|
||||
);
|
||||
|
||||
it.each(keywords)('should handle a "note for" with a %s in it', function (keyword: string) {
|
||||
const str = `classDiagram
|
||||
class Something {
|
||||
int id
|
||||
string name
|
||||
}
|
||||
note for Something "This is a keyword: ${keyword}. It truly is."
|
||||
`;
|
||||
|
||||
parser.parse(str);
|
||||
expect(classDb.getNotes()[0].text).toEqual(`This is a keyword: ${keyword}. It truly is.`);
|
||||
});
|
||||
|
||||
it.each(keywords)(
|
||||
'should handle a "note for" with a %s at beginning of string',
|
||||
function (keyword: string) {
|
||||
const str = `classDiagram
|
||||
class Something {
|
||||
int id
|
||||
string name
|
||||
}
|
||||
note for Something "${keyword}"
|
||||
`;
|
||||
|
||||
parser.parse(str);
|
||||
expect(classDb.getNotes()[0].text).toEqual(`${keyword}`);
|
||||
}
|
||||
);
|
||||
|
||||
it.each(keywords)('should elicit error for %s after NOTE token', function (keyword: string) {
|
||||
const str = `classDiagram
|
||||
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 () {
|
||||
@@ -315,8 +493,8 @@ class C13["With Città foreign language"]
|
||||
'classDiagram\n' +
|
||||
'class Class1 {\n' +
|
||||
'int testMember\n' +
|
||||
'string fooMember\n' +
|
||||
'test()\n' +
|
||||
'string fooMember\n' +
|
||||
'foo()\n' +
|
||||
'}';
|
||||
parser.parse(str);
|
||||
@@ -324,10 +502,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]).toBe('int testMember');
|
||||
expect(actual.members[1]).toBe('string fooMember');
|
||||
expect(actual.methods[0]).toBe('test()');
|
||||
expect(actual.methods[1]).toBe('foo()');
|
||||
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()');
|
||||
});
|
||||
|
||||
it('should parse a class with a text label and members', () => {
|
||||
@@ -337,7 +515,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]).toBe('+member1');
|
||||
expect(c1.members[0].getDisplayDetails().displayText).toBe('+member1');
|
||||
});
|
||||
|
||||
it('should parse a class with a text label, members and annotation', () => {
|
||||
@@ -352,7 +530,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]).toBe('+member1');
|
||||
expect(c1.members[0].getDisplayDetails().displayText).toBe('+member1');
|
||||
expect(c1.annotations.length).toBe(1);
|
||||
expect(c1.annotations[0]).toBe('interface');
|
||||
});
|
||||
@@ -635,6 +813,20 @@ describe('given a class diagram with members and methods ', function () {
|
||||
parser.parse(str);
|
||||
});
|
||||
|
||||
it('should handle direct member declaration', function () {
|
||||
parser.parse('classDiagram\n' + 'Car : wheels');
|
||||
const car = classDb.getClass('Car');
|
||||
expect(car.members.length).toBe(1);
|
||||
expect(car.members[0].id).toBe('wheels');
|
||||
});
|
||||
|
||||
it('should handle direct member declaration with type', function () {
|
||||
parser.parse('classDiagram\n' + 'Car : int wheels');
|
||||
const car = classDb.getClass('Car');
|
||||
expect(car.members.length).toBe(1);
|
||||
expect(car.members[0].id).toBe('int wheels');
|
||||
});
|
||||
|
||||
it('should handle simple member declaration with type', function () {
|
||||
const str = 'classDiagram\n' + 'class Car\n' + 'Car : int wheels';
|
||||
|
||||
@@ -655,10 +847,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]).toBe('-int privateMember');
|
||||
expect(actual.members[1]).toBe('+int publicMember');
|
||||
expect(actual.members[2]).toBe('#int protectedMember');
|
||||
expect(actual.members[3]).toBe('~int privatePackage');
|
||||
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');
|
||||
});
|
||||
|
||||
it('should handle generic types', function () {
|
||||
@@ -711,7 +903,9 @@ 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);
|
||||
expect(actual.methods[0]).toBe('someMethod()*');
|
||||
const method = actual.methods[0];
|
||||
expect(method.getDisplayDetails().displayText).toBe('someMethod()');
|
||||
expect(method.getDisplayDetails().cssStyle).toBe(abstractCssStyle);
|
||||
});
|
||||
|
||||
it('should handle static methods', function () {
|
||||
@@ -722,7 +916,9 @@ 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);
|
||||
expect(actual.methods[0]).toBe('someMethod()$');
|
||||
const method = actual.methods[0];
|
||||
expect(method.getDisplayDetails().displayText).toBe('someMethod()');
|
||||
expect(method.getDisplayDetails().cssStyle).toBe(staticCssStyle);
|
||||
});
|
||||
|
||||
it('should handle generic types in arguments', function () {
|
||||
@@ -848,53 +1044,6 @@ 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 () {
|
||||
@@ -1167,10 +1316,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]).toBe('int : test');
|
||||
expect(testClass.members[1]).toBe('string : foo');
|
||||
expect(testClass.methods[0]).toBe('test()');
|
||||
expect(testClass.methods[1]).toBe('foo()');
|
||||
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()');
|
||||
});
|
||||
|
||||
it('should handle abstract methods', function () {
|
||||
@@ -1181,7 +1330,9 @@ 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);
|
||||
expect(testClass.methods[0]).toBe('someMethod()*');
|
||||
const method = testClass.methods[0];
|
||||
expect(method.getDisplayDetails().displayText).toBe('someMethod()');
|
||||
expect(method.getDisplayDetails().cssStyle).toBe(abstractCssStyle);
|
||||
});
|
||||
|
||||
it('should handle static methods', function () {
|
||||
@@ -1192,7 +1343,9 @@ 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);
|
||||
expect(testClass.methods[0]).toBe('someMethod()$');
|
||||
const method = testClass.methods[0];
|
||||
expect(method.getDisplayDetails().displayText).toBe('someMethod()');
|
||||
expect(method.getDisplayDetails().cssStyle).toBe(staticCssStyle);
|
||||
});
|
||||
|
||||
it('should associate link and css appropriately', function () {
|
||||
@@ -1407,11 +1560,19 @@ 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]).toBe('+foo : string');
|
||||
expect(testNamespaceA.classes['A2'].members[0]).toBe('+bar : int');
|
||||
expect(testNamespaceA.classes['A1'].members[0].getDisplayDetails().displayText).toBe(
|
||||
'+foo : string'
|
||||
);
|
||||
expect(testNamespaceA.classes['A2'].members[0].getDisplayDetails().displayText).toBe(
|
||||
'+bar : int'
|
||||
);
|
||||
expect(Object.keys(testNamespaceB.classes).length).toBe(2);
|
||||
expect(testNamespaceB.classes['B1'].members[0]).toBe('+foo : bool');
|
||||
expect(testNamespaceB.classes['B2'].members[0]).toBe('+bar : float');
|
||||
expect(testNamespaceB.classes['B1'].members[0].getDisplayDetails().displayText).toBe(
|
||||
'+foo : bool'
|
||||
);
|
||||
expect(testNamespaceB.classes['B2'].members[0].getDisplayDetails().displayText).toBe(
|
||||
'+bar : float'
|
||||
);
|
||||
expect(Object.keys(testClasses).length).toBe(4);
|
||||
expect(testClasses['A1'].parent).toBe('A');
|
||||
expect(testClasses['A2'].parent).toBe('A');
|
||||
@@ -1463,8 +1624,8 @@ 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');
|
||||
|
||||
const member = c1.members[0];
|
||||
expect(member.getDisplayDetails().displayText).toBe('+member1');
|
||||
const c2 = classDb.getClass('C2');
|
||||
expect(c2.label).toBe('C2');
|
||||
});
|
||||
@@ -1480,9 +1641,10 @@ 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');
|
||||
@@ -1499,8 +1661,9 @@ 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', () => {
|
||||
@@ -1515,8 +1678,9 @@ 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', () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { DiagramDefinition } from '../../diagram-api/types.js';
|
||||
// @ts-ignore: TODO Fix ts errors
|
||||
import type { DiagramDefinition } from '../../diagram-api/types.js';
|
||||
// @ts-ignore: JISON doesn't support types
|
||||
import parser from './parser/classDiagram.jison';
|
||||
import db from './classDb.js';
|
||||
import styles from './styles.js';
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
// @ts-ignore - no types
|
||||
import { LALRGenerator } from 'jison';
|
||||
|
||||
const getAbsolutePath = (relativePath: string) => {
|
||||
return fileURLToPath(new URL(relativePath, import.meta.url));
|
||||
};
|
||||
|
||||
describe('class diagram grammar', function () {
|
||||
it('should have no conflicts', async function () {
|
||||
const grammarSource = await readFile(getAbsolutePath('./parser/classDiagram.jison'), 'utf8');
|
||||
const grammarParser = new LALRGenerator(grammarSource, {});
|
||||
expect(grammarParser.conflicts).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -1,78 +0,0 @@
|
||||
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",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
@@ -8,7 +8,8 @@ import utils from '../../utils.js';
|
||||
import { interpolateToCurve, getStylesFromArray } from '../../utils.js';
|
||||
import { setupGraphViewbox } from '../../setupGraphViewbox.js';
|
||||
import common from '../common/common.js';
|
||||
import { ClassRelation, ClassNote, ClassMap, EdgeData, NamespaceMap } from './classTypes.js';
|
||||
import type { ClassRelation, ClassNote, ClassMap, NamespaceMap } from './classTypes.js';
|
||||
import type { EdgeData } from '../../types.js';
|
||||
|
||||
const sanitizeText = (txt: string) => common.sanitizeText(txt, getConfig());
|
||||
|
||||
@@ -156,24 +157,17 @@ 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,
|
||||
@@ -301,7 +295,7 @@ export const setConf = function (cnf: any) {
|
||||
};
|
||||
|
||||
/**
|
||||
* Draws a flowchart in the tag with id: id based on the graph definition in text.
|
||||
* Draws a class diagram in the tag with id: id based on the definition in text.
|
||||
*
|
||||
* @param text -
|
||||
* @param id -
|
||||
|
||||
683
packages/mermaid/src/diagrams/class/classTypes.spec.ts
Normal file
683
packages/mermaid/src/diagrams/class/classTypes.spec.ts
Normal file
@@ -0,0 +1,683 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,10 +1,13 @@
|
||||
import { getConfig } from '../../config.js';
|
||||
import { parseGenericTypes, sanitizeText } from '../common/common.js';
|
||||
|
||||
export interface ClassNode {
|
||||
id: string;
|
||||
type: string;
|
||||
label: string;
|
||||
cssClasses: string[];
|
||||
methods: string[];
|
||||
members: string[];
|
||||
methods: ClassMember[];
|
||||
members: ClassMember[];
|
||||
annotations: string[];
|
||||
domId: string;
|
||||
parent?: string;
|
||||
@@ -14,30 +17,126 @@ 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;
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface EdgeData {
|
||||
arrowheadStyle?: string;
|
||||
labelpos?: string;
|
||||
labelType?: string;
|
||||
label?: string;
|
||||
classes: string;
|
||||
pattern: string;
|
||||
id: string;
|
||||
arrowhead: string;
|
||||
startLabelRight: string;
|
||||
endLabelLeft: string;
|
||||
arrowTypeStart: string;
|
||||
arrowTypeEnd: string;
|
||||
style: string;
|
||||
labelStyle: string;
|
||||
curve: any;
|
||||
}
|
||||
|
||||
export type ClassRelation = {
|
||||
id1: string;
|
||||
id2: string;
|
||||
|
||||
@@ -13,9 +13,6 @@
|
||||
%x href
|
||||
%x callback_name
|
||||
%x callback_args
|
||||
%x open_directive
|
||||
%x type_directive
|
||||
%x arg_directive
|
||||
%x acc_title
|
||||
%x acc_descr
|
||||
%x acc_descr_multiline
|
||||
@@ -24,31 +21,45 @@
|
||||
%x namespace
|
||||
%x namespace-body
|
||||
%%
|
||||
\%\%\{ { this.begin('open_directive'); return 'open_directive'; }
|
||||
.*direction\s+TB[^\n]* return 'direction_tb';
|
||||
.*direction\s+BT[^\n]* return 'direction_bt';
|
||||
.*direction\s+RL[^\n]* return 'direction_rl';
|
||||
.*direction\s+LR[^\n]* return 'direction_lr';
|
||||
<open_directive>((?:(?!\}\%\%)[^:.])*) { this.begin('type_directive'); return 'type_directive'; }
|
||||
<type_directive>":" { this.popState(); this.begin('arg_directive'); return ':'; }
|
||||
<type_directive,arg_directive>\}\%\% { this.popState(); this.popState(); return 'close_directive'; }
|
||||
<arg_directive>((?:(?!\}\%\%).|\n)*) return 'arg_directive';
|
||||
\%\%(?!\{)*[^\n]*(\r?\n?)+ /* skip comments */
|
||||
\%\%[^\n]*(\r?\n)* /* skip comments */
|
||||
accTitle\s*":"\s* { this.begin("acc_title");return 'acc_title'; }
|
||||
<acc_title>(?!\n|;|#)*[^\n]* { this.popState(); return "acc_title_value"; }
|
||||
accDescr\s*":"\s* { this.begin("acc_descr");return 'acc_descr'; }
|
||||
<acc_descr>(?!\n|;|#)*[^\n]* { this.popState(); return "acc_descr_value"; }
|
||||
accDescr\s*"{"\s* { this.begin("acc_descr_multiline");}
|
||||
<acc_descr_multiline>[\}] { this.popState(); }
|
||||
<acc_descr_multiline>[^\}]* return "acc_descr_multiline_value";
|
||||
.*direction\s+TB[^\n]* return 'direction_tb';
|
||||
.*direction\s+BT[^\n]* return 'direction_bt';
|
||||
.*direction\s+RL[^\n]* return 'direction_rl';
|
||||
.*direction\s+LR[^\n]* return 'direction_lr';
|
||||
\%\%(?!\{)*[^\n]*(\r?\n?)+ /* skip comments */
|
||||
\%\%[^\n]*(\r?\n)* /* skip comments */
|
||||
accTitle\s*":"\s* { this.begin("acc_title");return 'acc_title'; }
|
||||
<acc_title>(?!\n|;|#)*[^\n]* { this.popState(); return "acc_title_value"; }
|
||||
accDescr\s*":"\s* { this.begin("acc_descr");return 'acc_descr'; }
|
||||
<acc_descr>(?!\n|;|#)*[^\n]* { this.popState(); return "acc_descr_value"; }
|
||||
accDescr\s*"{"\s* { this.begin("acc_descr_multiline");}
|
||||
<acc_descr_multiline>[\}] { this.popState(); }
|
||||
<acc_descr_multiline>[^\}]* return "acc_descr_multiline_value";
|
||||
|
||||
\s*(\r?\n)+ return 'NEWLINE';
|
||||
\s+ /* skip whitespace */
|
||||
\s*(\r?\n)+ return 'NEWLINE';
|
||||
\s+ /* skip whitespace */
|
||||
|
||||
"classDiagram-v2" return 'CLASS_DIAGRAM';
|
||||
"classDiagram" return 'CLASS_DIAGRAM';
|
||||
"[*]" return 'EDGE_STATE';
|
||||
"classDiagram-v2" return 'CLASS_DIAGRAM';
|
||||
"classDiagram" return 'CLASS_DIAGRAM';
|
||||
"[*]" return 'EDGE_STATE';
|
||||
|
||||
/*
|
||||
---interactivity command---
|
||||
'call' adds a callback to the specified node. 'call' can only be specified when
|
||||
the line was introduced with 'click'.
|
||||
'call <callback_name>(<callback_args>)' attaches the function 'callback_name' with the specified
|
||||
arguments to the node that was specified by 'click'.
|
||||
Function arguments are optional: 'call <callback_name>()' simply executes 'callback_name' without any arguments.
|
||||
*/
|
||||
<INITIAL>"call"[\s]+ this.begin("callback_name");
|
||||
<callback_name>\([\s]*\) this.popState();
|
||||
<callback_name>\( this.popState(); this.begin("callback_args");
|
||||
<callback_name>[^(]* return 'CALLBACK_NAME';
|
||||
<callback_args>\) this.popState();
|
||||
<callback_args>[^)]* return 'CALLBACK_ARGS';
|
||||
|
||||
<string>["] this.popState();
|
||||
<string>[^"]* return "STR";
|
||||
<*>["] this.begin("string");
|
||||
|
||||
<INITIAL,namespace>"namespace" { this.begin('namespace'); return 'NAMESPACE'; }
|
||||
<namespace>\s*(\r?\n)+ { this.popState(); return 'NEWLINE'; }
|
||||
@@ -60,26 +71,26 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multili
|
||||
<namespace-body>\s+ /* skip whitespace */
|
||||
<namespace-body>"[*]" return 'EDGE_STATE';
|
||||
|
||||
<INITIAL,namespace-body>"class" { this.begin('class'); return 'CLASS';}
|
||||
<class>\s*(\r?\n)+ { this.popState(); return 'NEWLINE'; }
|
||||
<class>\s+ /* skip whitespace */
|
||||
<class>[}] { this.popState(); this.popState(); return 'STRUCT_STOP';}
|
||||
<class>[{] { this.begin("class-body"); return 'STRUCT_START';}
|
||||
<class-body>[}] { this.popState(); return 'STRUCT_STOP'; }
|
||||
<class-body><<EOF>> return "EOF_IN_STRUCT";
|
||||
<class-body>"[*]" { return 'EDGE_STATE';}
|
||||
<class-body>[{] return "OPEN_IN_STRUCT";
|
||||
<class-body>[\n] /* nothing */
|
||||
<class-body>[^{}\n]* { return "MEMBER";}
|
||||
<INITIAL,namespace-body>"class" { this.begin('class'); return 'CLASS';}
|
||||
<class>\s*(\r?\n)+ { this.popState(); return 'NEWLINE'; }
|
||||
<class>\s+ /* skip whitespace */
|
||||
<class>[}] { this.popState(); this.popState(); return 'STRUCT_STOP';}
|
||||
<class>[{] { this.begin("class-body"); return 'STRUCT_START';}
|
||||
<class-body>[}] { this.popState(); return 'STRUCT_STOP'; }
|
||||
<class-body><<EOF>> return "EOF_IN_STRUCT";
|
||||
<class-body>"[*]" { return 'EDGE_STATE';}
|
||||
<class-body>[{] return "OPEN_IN_STRUCT";
|
||||
<class-body>[\n] /* nothing */
|
||||
<class-body>[^{}\n]* { return "MEMBER";}
|
||||
|
||||
<*>"cssClass" return 'CSSCLASS';
|
||||
<*>"callback" return 'CALLBACK';
|
||||
<*>"link" return 'LINK';
|
||||
<*>"click" return 'CLICK';
|
||||
<*>"note for" return 'NOTE_FOR';
|
||||
<*>"note" return 'NOTE';
|
||||
<*>"<<" return 'ANNOTATION_START';
|
||||
<*>">>" return 'ANNOTATION_END';
|
||||
<*>"cssClass" return 'CSSCLASS';
|
||||
<*>"callback" return 'CALLBACK';
|
||||
<*>"link" return 'LINK';
|
||||
<*>"click" return 'CLICK';
|
||||
<*>"note for" return 'NOTE_FOR';
|
||||
<*>"note" return 'NOTE';
|
||||
<*>"<<" return 'ANNOTATION_START';
|
||||
<*>">>" return 'ANNOTATION_END';
|
||||
|
||||
/*
|
||||
---interactivity command---
|
||||
@@ -87,64 +98,43 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multili
|
||||
line was introduced with 'click'.
|
||||
'href "<link>"' attaches the specified link to the node that was specified by 'click'.
|
||||
*/
|
||||
<*>"href"[\s]+["] this.begin("href");
|
||||
<href>["] this.popState();
|
||||
<href>[^"]* return 'HREF';
|
||||
<*>"href" return 'HREF';
|
||||
|
||||
/*
|
||||
---interactivity command---
|
||||
'call' adds a callback to the specified node. 'call' can only be specified when
|
||||
the line was introduced with 'click'.
|
||||
'call <callback_name>(<callback_args>)' attaches the function 'callback_name' with the specified
|
||||
arguments to the node that was specified by 'click'.
|
||||
Function arguments are optional: 'call <callback_name>()' simply executes 'callback_name' without any arguments.
|
||||
*/
|
||||
<*>"call"[\s]+ this.begin("callback_name");
|
||||
<callback_name>\([\s]*\) this.popState();
|
||||
<callback_name>\( this.popState(); this.begin("callback_args");
|
||||
<callback_name>[^(]* return 'CALLBACK_NAME';
|
||||
<callback_args>\) this.popState();
|
||||
<callback_args>[^)]* return 'CALLBACK_ARGS';
|
||||
<generic>[~] this.popState();
|
||||
<generic>[^~]* return "GENERICTYPE";
|
||||
<*>"~" this.begin("generic");
|
||||
|
||||
<generic>[~] this.popState();
|
||||
<generic>[^~]* return "GENERICTYPE";
|
||||
<*>[~] this.begin("generic");
|
||||
<bqstring>[`] this.popState();
|
||||
<bqstring>[^`]+ return "BQUOTE_STR";
|
||||
<*>[`] this.begin("bqstring");
|
||||
|
||||
<string>["] this.popState();
|
||||
<string>[^"]* return "STR";
|
||||
<*>["] this.begin("string");
|
||||
<*>"_self" return 'LINK_TARGET';
|
||||
<*>"_blank" return 'LINK_TARGET';
|
||||
<*>"_parent" return 'LINK_TARGET';
|
||||
<*>"_top" return 'LINK_TARGET';
|
||||
|
||||
<bqstring>[`] this.popState();
|
||||
<bqstring>[^`]+ return "BQUOTE_STR";
|
||||
<*>[`] this.begin("bqstring");
|
||||
|
||||
<*>"_self" return 'LINK_TARGET';
|
||||
<*>"_blank" return 'LINK_TARGET';
|
||||
<*>"_parent" return 'LINK_TARGET';
|
||||
<*>"_top" return 'LINK_TARGET';
|
||||
|
||||
<*>\s*\<\| return 'EXTENSION';
|
||||
<*>\s*\|\> return 'EXTENSION';
|
||||
<*>\s*\> return 'DEPENDENCY';
|
||||
<*>\s*\< return 'DEPENDENCY';
|
||||
<*>\s*\* return 'COMPOSITION';
|
||||
<*>\s*o return 'AGGREGATION';
|
||||
<*>\s*\(\) return 'LOLLIPOP';
|
||||
<*>\-\- return 'LINE';
|
||||
<*>\.\. return 'DOTTED_LINE';
|
||||
<*>":"{1}[^:\n;]+ return 'LABEL';
|
||||
<*>":"{3} return 'STYLE_SEPARATOR';
|
||||
<*>\- return 'MINUS';
|
||||
<*>"." return 'DOT';
|
||||
<*>\+ return 'PLUS';
|
||||
<*>\% return 'PCT';
|
||||
<*>"=" return 'EQUALS';
|
||||
<*>\= return 'EQUALS';
|
||||
<*>\w+ return 'ALPHA';
|
||||
<*>"[" return 'SQS';
|
||||
<*>"]" return 'SQE';
|
||||
<*>[!"#$%&'*+,-.`?\\/] return 'PUNCTUATION';
|
||||
<*>[0-9]+ return 'NUM';
|
||||
<*>\s*\<\| return 'EXTENSION';
|
||||
<*>\s*\|\> return 'EXTENSION';
|
||||
<*>\s*\> return 'DEPENDENCY';
|
||||
<*>\s*\< return 'DEPENDENCY';
|
||||
<*>\s*\* return 'COMPOSITION';
|
||||
<*>\s*o return 'AGGREGATION';
|
||||
<*>\s*\(\) return 'LOLLIPOP';
|
||||
<*>\-\- return 'LINE';
|
||||
<*>\.\. return 'DOTTED_LINE';
|
||||
<*>":"{1}[^:\n;]+ return 'LABEL';
|
||||
<*>":"{3} return 'STYLE_SEPARATOR';
|
||||
<*>\- return 'MINUS';
|
||||
<*>"." return 'DOT';
|
||||
<*>\+ return 'PLUS';
|
||||
<*>\% return 'PCT';
|
||||
<*>"=" return 'EQUALS';
|
||||
<*>\= return 'EQUALS';
|
||||
<*>\w+ return 'ALPHA';
|
||||
<*>"[" return 'SQS';
|
||||
<*>"]" return 'SQE';
|
||||
<*>[!"#$%&'*+,-.`?\\/] return 'PUNCTUATION';
|
||||
<*>[0-9]+ return 'NUM';
|
||||
<*>[\u00AA\u00B5\u00BA\u00C0-\u00D6\u00D8-\u00F6]|
|
||||
[\u00F8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377]|
|
||||
[\u037A-\u037D\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5]|
|
||||
@@ -206,9 +196,9 @@ Function arguments are optional: 'call <callback_name>()' simply executes 'callb
|
||||
[\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC]|
|
||||
[\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF]|
|
||||
[\uFFD2-\uFFD7\uFFDA-\uFFDC]
|
||||
return 'UNICODE_TEXT';
|
||||
<*>\s return 'SPACE';
|
||||
<*><<EOF>> return 'EOF';
|
||||
return 'UNICODE_TEXT';
|
||||
<*>\s return 'SPACE';
|
||||
<*><<EOF>> return 'EOF';
|
||||
|
||||
/lex
|
||||
|
||||
@@ -222,46 +212,13 @@ Function arguments are optional: 'call <callback_name>()' simply executes 'callb
|
||||
|
||||
start
|
||||
: mermaidDoc
|
||||
| directive start
|
||||
| statements
|
||||
;
|
||||
|
||||
direction
|
||||
: direction_tb
|
||||
{ yy.setDirection('TB');}
|
||||
| direction_bt
|
||||
{ yy.setDirection('BT');}
|
||||
| direction_rl
|
||||
{ yy.setDirection('RL');}
|
||||
| direction_lr
|
||||
{ yy.setDirection('LR');}
|
||||
;
|
||||
|
||||
mermaidDoc
|
||||
: graphConfig
|
||||
;
|
||||
|
||||
directive
|
||||
: openDirective typeDirective closeDirective NEWLINE
|
||||
| openDirective typeDirective ':' argDirective closeDirective NEWLINE
|
||||
;
|
||||
|
||||
openDirective
|
||||
: open_directive { yy.parseDirective('%%{', 'open_directive'); }
|
||||
;
|
||||
|
||||
typeDirective
|
||||
: type_directive { yy.parseDirective($1, 'type_directive'); }
|
||||
;
|
||||
|
||||
argDirective
|
||||
: arg_directive { $1 = $1.trim().replace(/'/g, '"'); yy.parseDirective($1, 'arg_directive'); }
|
||||
;
|
||||
|
||||
closeDirective
|
||||
: close_directive { yy.parseDirective('}%%', 'close_directive', 'class'); }
|
||||
;
|
||||
|
||||
graphConfig
|
||||
: CLASS_DIAGRAM NEWLINE statements EOF
|
||||
;
|
||||
@@ -294,7 +251,7 @@ statement
|
||||
| relationStatement LABEL { $1.title = yy.cleanupLabel($2); yy.addRelation($1); }
|
||||
| namespaceStatement
|
||||
| classStatement
|
||||
| methodStatement
|
||||
| memberStatement
|
||||
| annotationStatement
|
||||
| clickStatement
|
||||
| cssClassStatement
|
||||
@@ -321,7 +278,7 @@ classStatements
|
||||
;
|
||||
|
||||
classStatement
|
||||
: classIdentifier
|
||||
: classIdentifier
|
||||
| classIdentifier STYLE_SEPARATOR alphaNumToken {yy.setCssClass($1, $3);}
|
||||
| classIdentifier STRUCT_START members STRUCT_STOP {yy.addMembers($1,$3);}
|
||||
| classIdentifier STYLE_SEPARATOR alphaNumToken STRUCT_START members STRUCT_STOP {yy.setCssClass($1, $3);yy.addMembers($1,$5);}
|
||||
@@ -341,7 +298,7 @@ members
|
||||
| MEMBER members { $2.push($1);$$=$2;}
|
||||
;
|
||||
|
||||
methodStatement
|
||||
memberStatement
|
||||
: className {/*console.log('Rel found',$1);*/}
|
||||
| className LABEL {yy.addMember($1,yy.cleanupLabel($2));}
|
||||
| MEMBER {/*console.warn('Member',$1);*/}
|
||||
@@ -360,6 +317,17 @@ 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}; }
|
||||
@@ -391,10 +359,10 @@ clickStatement
|
||||
| CLICK className CALLBACK_NAME STR {$$ = $1;yy.setClickEvent($2, $3);yy.setTooltip($2, $4);}
|
||||
| CLICK className CALLBACK_NAME CALLBACK_ARGS {$$ = $1;yy.setClickEvent($2, $3, $4);}
|
||||
| CLICK className CALLBACK_NAME CALLBACK_ARGS STR {$$ = $1;yy.setClickEvent($2, $3, $4);yy.setTooltip($2, $5);}
|
||||
| CLICK className HREF {$$ = $1;yy.setLink($2, $3);}
|
||||
| CLICK className HREF LINK_TARGET {$$ = $1;yy.setLink($2, $3, $4);}
|
||||
| CLICK className HREF STR {$$ = $1;yy.setLink($2, $3);yy.setTooltip($2, $4);}
|
||||
| CLICK className HREF STR LINK_TARGET {$$ = $1;yy.setLink($2, $3, $5);yy.setTooltip($2, $4);}
|
||||
| CLICK className HREF STR {$$ = $1;yy.setLink($2, $4);}
|
||||
| CLICK className HREF STR LINK_TARGET {$$ = $1;yy.setLink($2, $4, $5);}
|
||||
| CLICK className HREF STR STR {$$ = $1;yy.setLink($2, $4);yy.setTooltip($2, $5);}
|
||||
| CLICK className HREF STR STR LINK_TARGET {$$ = $1;yy.setLink($2, $4, $6);yy.setTooltip($2, $5);}
|
||||
;
|
||||
|
||||
cssClassStatement
|
||||
|
||||
@@ -109,25 +109,25 @@ g.classGroup line {
|
||||
}
|
||||
|
||||
#extensionStart, .extension {
|
||||
fill: ${options.mainBkg} !important;
|
||||
fill: transparent !important;
|
||||
stroke: ${options.lineColor} !important;
|
||||
stroke-width: 1;
|
||||
}
|
||||
|
||||
#extensionEnd, .extension {
|
||||
fill: ${options.mainBkg} !important;
|
||||
fill: transparent !important;
|
||||
stroke: ${options.lineColor} !important;
|
||||
stroke-width: 1;
|
||||
}
|
||||
|
||||
#aggregationStart, .aggregation {
|
||||
fill: ${options.mainBkg} !important;
|
||||
fill: transparent !important;
|
||||
stroke: ${options.lineColor} !important;
|
||||
stroke-width: 1;
|
||||
}
|
||||
|
||||
#aggregationEnd, .aggregation {
|
||||
fill: ${options.mainBkg} !important;
|
||||
fill: transparent !important;
|
||||
stroke: ${options.lineColor} !important;
|
||||
stroke-width: 1;
|
||||
}
|
||||
|
||||
@@ -172,7 +172,6 @@ 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
|
||||
@@ -209,47 +208,56 @@ export const drawClass = function (elem, classDef, conf, diagObj) {
|
||||
}
|
||||
|
||||
const titleHeight = title.node().getBBox().height;
|
||||
let membersLine;
|
||||
let membersBox;
|
||||
let methodsLine;
|
||||
|
||||
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);
|
||||
// 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 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;
|
||||
});
|
||||
|
||||
const membersBox = members.node().getBBox();
|
||||
membersBox = members.node().getBBox();
|
||||
}
|
||||
|
||||
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);
|
||||
// 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 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 = ' ';
|
||||
@@ -278,8 +286,12 @@ export const drawClass = function (elem, classDef, conf, diagObj) {
|
||||
title.insert('title').text(classDef.tooltip);
|
||||
}
|
||||
|
||||
membersLine.attr('x2', rectWidth);
|
||||
methodsLine.attr('x2', rectWidth);
|
||||
if (membersLine) {
|
||||
membersLine.attr('x2', rectWidth);
|
||||
}
|
||||
if (methodsLine) {
|
||||
methodsLine.attr('x2', rectWidth);
|
||||
}
|
||||
|
||||
classInfo.width = rectWidth;
|
||||
classInfo.height = classBox.height + conf.padding + 0.5 * conf.dividerMargin;
|
||||
@@ -291,7 +303,7 @@ export const getClassTitleString = function (classDef) {
|
||||
let classTitleString = classDef.id;
|
||||
|
||||
if (classDef.type) {
|
||||
classTitleString += '<' + classDef.type + '>';
|
||||
classTitleString += '<' + parseGenericTypes(classDef.type) + '>';
|
||||
}
|
||||
|
||||
return classTitleString;
|
||||
@@ -360,82 +372,19 @@ 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} txt The member
|
||||
* @param {string} member The member
|
||||
* @param {boolean} isFirst
|
||||
* @param {{ padding: string; textHeight: string }} conf The configuration for the member
|
||||
*/
|
||||
const addTspan = function (textEl, txt, isFirst, conf) {
|
||||
let member = parseMember(txt);
|
||||
const addTspan = function (textEl, member, isFirst, conf) {
|
||||
const { displayText, cssStyle } = member.getDisplayDetails();
|
||||
const tSpan = textEl.append('tspan').attr('x', conf.padding).text(displayText);
|
||||
|
||||
const tSpan = textEl.append('tspan').attr('x', conf.padding).text(member.displayText);
|
||||
|
||||
if (member.cssStyle !== '') {
|
||||
if (cssStyle !== '') {
|
||||
tSpan.attr('style', member.cssStyle);
|
||||
}
|
||||
|
||||
@@ -444,27 +393,9 @@ const addTspan = function (textEl, txt, 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,
|
||||
};
|
||||
|
||||
@@ -1,330 +1,27 @@
|
||||
import svgDraw from './svgDraw.js';
|
||||
import { JSDOM } from 'jsdom';
|
||||
|
||||
describe('given a string representing class method, ', function () {
|
||||
it('should handle class names with generics', function () {
|
||||
const classDef = {
|
||||
id: 'Car',
|
||||
type: 'T',
|
||||
label: 'Car',
|
||||
};
|
||||
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',
|
||||
};
|
||||
|
||||
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('');
|
||||
let actual = svgDraw.getClassTitleString(classDef);
|
||||
expect(actual).toBe('Car<T>');
|
||||
});
|
||||
it('should return correct text for nested generics', function () {
|
||||
const classDef = {
|
||||
id: 'Car',
|
||||
type: 'T~T~',
|
||||
label: 'Car',
|
||||
};
|
||||
|
||||
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('');
|
||||
let actual = svgDraw.getClassTitleString(classDef);
|
||||
expect(actual).toBe('Car<T<T>>');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
import { sanitizeText, removeScript, parseGenericTypes } from './common.js';
|
||||
|
||||
describe('when securityLevel is antiscript, all script must be removed', function () {
|
||||
/**
|
||||
* @param {string} original The original text
|
||||
* @param {string} result The expected sanitized text
|
||||
*/
|
||||
function compareRemoveScript(original, result) {
|
||||
expect(removeScript(original).trim()).toEqual(result);
|
||||
}
|
||||
|
||||
it('should remove all script block, script inline.', function () {
|
||||
const labelString = `1
|
||||
Act1: Hello 1<script src="http://abc.com/script1.js"></script>1
|
||||
<b>Act2</b>:
|
||||
1<script>
|
||||
alert('script run......');
|
||||
</script>1
|
||||
1`;
|
||||
const exactlyString = `1
|
||||
Act1: Hello 11
|
||||
<b>Act2</b>:
|
||||
11
|
||||
1`;
|
||||
compareRemoveScript(labelString, exactlyString);
|
||||
});
|
||||
|
||||
it('should remove all javascript urls', function () {
|
||||
compareRemoveScript(
|
||||
`This is a <a href="javascript:runHijackingScript();">clean link</a> + <a href="javascript:runHijackingScript();">clean link</a>
|
||||
and <a href="javascript:bipassedMining();">me too</a>`,
|
||||
`This is a <a>clean link</a> + <a>clean link</a>
|
||||
and <a>me too</a>`
|
||||
);
|
||||
});
|
||||
|
||||
it('should detect malicious images', function () {
|
||||
compareRemoveScript(`<img onerror="alert('hello');">`, `<img>`);
|
||||
});
|
||||
|
||||
it('should detect iframes', function () {
|
||||
compareRemoveScript(
|
||||
`<iframe src="http://abc.com/script1.js"></iframe>
|
||||
<iframe src="http://example.com/iframeexample"></iframe>`,
|
||||
''
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Sanitize text', function () {
|
||||
it('should remove script tag', function () {
|
||||
const maliciousStr = 'javajavascript:script:alert(1)';
|
||||
const result = sanitizeText(maliciousStr, {
|
||||
securityLevel: 'strict',
|
||||
flowchart: { htmlLabels: true },
|
||||
});
|
||||
expect(result).not.toContain('javascript:alert(1)');
|
||||
});
|
||||
});
|
||||
|
||||
describe('generic parser', function () {
|
||||
it('should parse generic types', function () {
|
||||
expect(parseGenericTypes('test~T~')).toEqual('test<T>');
|
||||
expect(parseGenericTypes('test~Array~Array~string~~~')).toEqual('test<Array<Array<string>>>');
|
||||
expect(parseGenericTypes('test~Array~Array~string[]~~~')).toEqual(
|
||||
'test<Array<Array<string[]>>>'
|
||||
);
|
||||
expect(parseGenericTypes('test ~Array~Array~string[]~~~')).toEqual(
|
||||
'test <Array<Array<string[]>>>'
|
||||
);
|
||||
expect(parseGenericTypes('~test')).toEqual('~test');
|
||||
expect(parseGenericTypes('~test Array~string~')).toEqual('~test Array<string>');
|
||||
});
|
||||
});
|
||||
87
packages/mermaid/src/diagrams/common/common.spec.ts
Normal file
87
packages/mermaid/src/diagrams/common/common.spec.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { sanitizeText, removeScript, parseGenericTypes, countOccurrence } from './common.js';
|
||||
|
||||
describe('when securityLevel is antiscript, all script must be removed', () => {
|
||||
/**
|
||||
* @param original - The original text
|
||||
* @param result - The expected sanitized text
|
||||
*/
|
||||
function compareRemoveScript(original: string, result: string) {
|
||||
expect(removeScript(original).trim()).toEqual(result);
|
||||
}
|
||||
|
||||
it('should remove all script block, script inline.', () => {
|
||||
const labelString = `1
|
||||
Act1: Hello 1<script src="http://abc.com/script1.js"></script>1
|
||||
<b>Act2</b>:
|
||||
1<script>
|
||||
alert('script run......');
|
||||
</script>1
|
||||
1`;
|
||||
const exactlyString = `1
|
||||
Act1: Hello 11
|
||||
<b>Act2</b>:
|
||||
11
|
||||
1`;
|
||||
compareRemoveScript(labelString, exactlyString);
|
||||
});
|
||||
|
||||
it('should remove all javascript urls', () => {
|
||||
compareRemoveScript(
|
||||
`This is a <a href="javascript:runHijackingScript();">clean link</a> + <a href="javascript:runHijackingScript();">clean link</a>
|
||||
and <a href="javascript:bipassedMining();">me too</a>`,
|
||||
`This is a <a>clean link</a> + <a>clean link</a>
|
||||
and <a>me too</a>`
|
||||
);
|
||||
});
|
||||
|
||||
it('should detect malicious images', () => {
|
||||
compareRemoveScript(`<img onerror="alert('hello');">`, `<img>`);
|
||||
});
|
||||
|
||||
it('should detect iframes', () => {
|
||||
compareRemoveScript(
|
||||
`<iframe src="http://abc.com/script1.js"></iframe>
|
||||
<iframe src="http://example.com/iframeexample"></iframe>`,
|
||||
''
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Sanitize text', () => {
|
||||
it('should remove script tag', () => {
|
||||
const maliciousStr = 'javajavascript:script:alert(1)';
|
||||
const result = sanitizeText(maliciousStr, {
|
||||
securityLevel: 'strict',
|
||||
flowchart: { htmlLabels: true },
|
||||
});
|
||||
expect(result).not.toContain('javascript:alert(1)');
|
||||
});
|
||||
});
|
||||
|
||||
describe('generic parser', () => {
|
||||
it.each([
|
||||
['test~T~', 'test<T>'],
|
||||
['test~Array~Array~string~~~', 'test<Array<Array<string>>>'],
|
||||
['test~Array~Array~string[]~~~', 'test<Array<Array<string[]>>>'],
|
||||
['test ~Array~Array~string[]~~~', 'test <Array<Array<string[]>>>'],
|
||||
['~test', '~test'],
|
||||
['~test~T~', '~test<T>'],
|
||||
])('should parse generic types: %s to %s', (input: string, expected: string) => {
|
||||
expect(parseGenericTypes(input)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
['', '', 0],
|
||||
['', 'x', 0],
|
||||
['test', 'x', 0],
|
||||
['test', 't', 2],
|
||||
['test', 'te', 1],
|
||||
['test~T~', '~', 2],
|
||||
['test~Array~Array~string~~~', '~', 6],
|
||||
])(
|
||||
'should count `%s` to contain occurrences of `%s` to be `%i`',
|
||||
(str: string, substring: string, count: number) => {
|
||||
expect(countOccurrence(str, substring)).toEqual(count);
|
||||
}
|
||||
);
|
||||
@@ -1,6 +1,7 @@
|
||||
import DOMPurify from 'dompurify';
|
||||
import { MermaidConfig } from '../../config.type.js';
|
||||
import type { MermaidConfig } from '../../config.type.js';
|
||||
|
||||
// Remove and ignore br:s
|
||||
export const lineBreakRegex = /<br\s*\/?>/gi;
|
||||
|
||||
/**
|
||||
@@ -177,23 +178,80 @@ export const getMin = function (...values: number[]): number {
|
||||
* @param text - The text to convert
|
||||
* @returns The converted string
|
||||
*/
|
||||
export const parseGenericTypes = function (text: string): string {
|
||||
let cleanedText = text;
|
||||
export const parseGenericTypes = function (input: string): string {
|
||||
const inputSets = input.split(/(,)/);
|
||||
const output = [];
|
||||
|
||||
if (text.split('~').length - 1 >= 2) {
|
||||
let newCleanedText = cleanedText;
|
||||
for (let i = 0; i < inputSets.length; i++) {
|
||||
let thisSet = inputSets[i];
|
||||
|
||||
// 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 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];
|
||||
|
||||
return parseGenericTypes(newCleanedText);
|
||||
} else {
|
||||
return 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 output.join('');
|
||||
};
|
||||
|
||||
export const countOccurrence = (string: string, substring: string): number => {
|
||||
return Math.max(0, string.split(substring).length - 1);
|
||||
};
|
||||
|
||||
const shouldCombineSets = (previousSet: string, nextSet: string): boolean => {
|
||||
const prevCount = countOccurrence(previousSet, '~');
|
||||
const nextCount = countOccurrence(nextSet, '~');
|
||||
|
||||
return prevCount === 1 && nextCount === 1;
|
||||
};
|
||||
|
||||
const processSet = (input: string): string => {
|
||||
const tildeCount = countOccurrence(input, '~');
|
||||
let hasStartingTilde = false;
|
||||
|
||||
if (tildeCount <= 1) {
|
||||
return input;
|
||||
}
|
||||
|
||||
// If there is an odd number of tildes, and the input starts with a tilde, we need to remove it and add it back in later
|
||||
if (tildeCount % 2 !== 0 && input.startsWith('~')) {
|
||||
input = input.substring(1);
|
||||
hasStartingTilde = true;
|
||||
}
|
||||
|
||||
const chars = [...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('~');
|
||||
}
|
||||
|
||||
// Add the starting tilde back in if we removed it
|
||||
if (hasStartingTilde) {
|
||||
chars.unshift('~');
|
||||
}
|
||||
|
||||
return chars.join('');
|
||||
};
|
||||
|
||||
// TODO: find a better method for detecting support. This interface was added in the MathML 4 spec.
|
||||
|
||||
32
packages/mermaid/src/diagrams/common/commonDb.ts
Normal file
32
packages/mermaid/src/diagrams/common/commonDb.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { sanitizeText as _sanitizeText } from './common.js';
|
||||
import { getConfig } from '../../config.js';
|
||||
|
||||
let accTitle = '';
|
||||
let diagramTitle = '';
|
||||
let accDescription = '';
|
||||
|
||||
const sanitizeText = (txt: string): string => _sanitizeText(txt, getConfig());
|
||||
|
||||
export const clear = (): void => {
|
||||
accTitle = '';
|
||||
accDescription = '';
|
||||
diagramTitle = '';
|
||||
};
|
||||
|
||||
export const setAccTitle = (txt: string): void => {
|
||||
accTitle = sanitizeText(txt).replace(/^\s+/g, '');
|
||||
};
|
||||
|
||||
export const getAccTitle = (): string => accTitle;
|
||||
|
||||
export const setAccDescription = (txt: string): void => {
|
||||
accDescription = sanitizeText(txt).replace(/\n\s+/g, '\n');
|
||||
};
|
||||
|
||||
export const getAccDescription = (): string => accDescription;
|
||||
|
||||
export const setDiagramTitle = (txt: string): void => {
|
||||
diagramTitle = sanitizeText(txt);
|
||||
};
|
||||
|
||||
export const getDiagramTitle = (): string => diagramTitle;
|
||||
58
packages/mermaid/src/diagrams/common/commonTypes.ts
Normal file
58
packages/mermaid/src/diagrams/common/commonTypes.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
export interface RectData {
|
||||
x: number;
|
||||
y: number;
|
||||
fill: string;
|
||||
width: number;
|
||||
height: number;
|
||||
stroke: string;
|
||||
class?: string;
|
||||
color?: string;
|
||||
rx?: number;
|
||||
ry?: number;
|
||||
attrs?: Record<string, string | number>;
|
||||
anchor?: string;
|
||||
}
|
||||
|
||||
export interface Bound {
|
||||
startx: number;
|
||||
stopx: number;
|
||||
starty: number;
|
||||
stopy: number;
|
||||
fill: string;
|
||||
stroke: string;
|
||||
}
|
||||
|
||||
export interface TextData {
|
||||
x: number;
|
||||
y: number;
|
||||
anchor: string;
|
||||
text: string;
|
||||
textMargin: number;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
export interface TextObject {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
fill?: string;
|
||||
anchor?: string;
|
||||
'text-anchor': string;
|
||||
style: string;
|
||||
textMargin: number;
|
||||
rx: number;
|
||||
ry: number;
|
||||
tspan: boolean;
|
||||
valign?: string;
|
||||
}
|
||||
|
||||
export type D3RectElement = d3.Selection<SVGRectElement, unknown, Element | null, unknown>;
|
||||
|
||||
export type D3UseElement = d3.Selection<SVGUseElement, unknown, Element | null, unknown>;
|
||||
|
||||
export type D3ImageElement = d3.Selection<SVGImageElement, unknown, Element | null, unknown>;
|
||||
|
||||
export type D3TextElement = d3.Selection<SVGTextElement, unknown, Element | null, unknown>;
|
||||
|
||||
export type D3TSpanElement = d3.Selection<SVGTSpanElement, unknown, Element | null, unknown>;
|
||||
@@ -1,114 +0,0 @@
|
||||
import { sanitizeUrl } from '@braintree/sanitize-url';
|
||||
|
||||
export const drawRect = function (elem, rectData) {
|
||||
const rectElem = elem.append('rect');
|
||||
rectElem.attr('x', rectData.x);
|
||||
rectElem.attr('y', rectData.y);
|
||||
rectElem.attr('fill', rectData.fill);
|
||||
rectElem.attr('stroke', rectData.stroke);
|
||||
rectElem.attr('width', rectData.width);
|
||||
rectElem.attr('height', rectData.height);
|
||||
rectElem.attr('rx', rectData.rx);
|
||||
rectElem.attr('ry', rectData.ry);
|
||||
|
||||
if (rectData.attrs !== 'undefined' && rectData.attrs !== null) {
|
||||
for (let attrKey in rectData.attrs) {
|
||||
rectElem.attr(attrKey, rectData.attrs[attrKey]);
|
||||
}
|
||||
}
|
||||
|
||||
if (rectData.class !== 'undefined') {
|
||||
rectElem.attr('class', rectData.class);
|
||||
}
|
||||
|
||||
return rectElem;
|
||||
};
|
||||
|
||||
/**
|
||||
* Draws a background rectangle
|
||||
*
|
||||
* @param {any} elem Diagram (reference for bounds)
|
||||
* @param {any} bounds Shape of the rectangle
|
||||
*/
|
||||
export const drawBackgroundRect = function (elem, bounds) {
|
||||
const rectElem = drawRect(elem, {
|
||||
x: bounds.startx,
|
||||
y: bounds.starty,
|
||||
width: bounds.stopx - bounds.startx,
|
||||
height: bounds.stopy - bounds.starty,
|
||||
fill: bounds.fill,
|
||||
stroke: bounds.stroke,
|
||||
class: 'rect',
|
||||
});
|
||||
rectElem.lower();
|
||||
};
|
||||
|
||||
export const drawText = function (elem, textData) {
|
||||
// Remove and ignore br:s
|
||||
const nText = textData.text.replace(/<br\s*\/?>/gi, ' ');
|
||||
|
||||
const textElem = elem.append('text');
|
||||
textElem.attr('x', textData.x);
|
||||
textElem.attr('y', textData.y);
|
||||
textElem.attr('class', 'legend');
|
||||
|
||||
textElem.style('text-anchor', textData.anchor);
|
||||
|
||||
if (textData.class !== undefined) {
|
||||
textElem.attr('class', textData.class);
|
||||
}
|
||||
|
||||
const span = textElem.append('tspan');
|
||||
span.attr('x', textData.x + textData.textMargin * 2);
|
||||
span.text(nText);
|
||||
|
||||
return textElem;
|
||||
};
|
||||
|
||||
export const drawImage = function (elem, x, y, link) {
|
||||
const imageElem = elem.append('image');
|
||||
imageElem.attr('x', x);
|
||||
imageElem.attr('y', y);
|
||||
var sanitizedLink = sanitizeUrl(link);
|
||||
imageElem.attr('xlink:href', sanitizedLink);
|
||||
};
|
||||
|
||||
export const drawEmbeddedImage = function (elem, x, y, link) {
|
||||
const imageElem = elem.append('use');
|
||||
imageElem.attr('x', x);
|
||||
imageElem.attr('y', y);
|
||||
const sanitizedLink = sanitizeUrl(link);
|
||||
imageElem.attr('xlink:href', '#' + sanitizedLink);
|
||||
};
|
||||
|
||||
export const getNoteRect = function () {
|
||||
return {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 100,
|
||||
height: 100,
|
||||
fill: '#EDF2AE',
|
||||
stroke: '#666',
|
||||
anchor: 'start',
|
||||
rx: 0,
|
||||
ry: 0,
|
||||
};
|
||||
};
|
||||
|
||||
export const getTextObj = function () {
|
||||
return {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 100,
|
||||
height: 100,
|
||||
fill: undefined,
|
||||
anchor: undefined,
|
||||
'text-anchor': 'start',
|
||||
style: '#666',
|
||||
textMargin: 0,
|
||||
rx: 0,
|
||||
ry: 0,
|
||||
tspan: true,
|
||||
valign: undefined,
|
||||
};
|
||||
};
|
||||
126
packages/mermaid/src/diagrams/common/svgDrawCommon.ts
Normal file
126
packages/mermaid/src/diagrams/common/svgDrawCommon.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
import { sanitizeUrl } from '@braintree/sanitize-url';
|
||||
import type { Group, SVG } from '../../diagram-api/types.js';
|
||||
import type {
|
||||
Bound,
|
||||
D3ImageElement,
|
||||
D3RectElement,
|
||||
D3TSpanElement,
|
||||
D3TextElement,
|
||||
D3UseElement,
|
||||
RectData,
|
||||
TextData,
|
||||
TextObject,
|
||||
} from './commonTypes.js';
|
||||
import { lineBreakRegex } from './common.js';
|
||||
|
||||
export const drawRect = (element: SVG | Group, rectData: RectData): D3RectElement => {
|
||||
const rectElement: D3RectElement = element.append('rect');
|
||||
rectElement.attr('x', rectData.x);
|
||||
rectElement.attr('y', rectData.y);
|
||||
rectElement.attr('fill', rectData.fill);
|
||||
rectElement.attr('stroke', rectData.stroke);
|
||||
rectElement.attr('width', rectData.width);
|
||||
rectElement.attr('height', rectData.height);
|
||||
rectData.rx !== undefined && rectElement.attr('rx', rectData.rx);
|
||||
rectData.ry !== undefined && rectElement.attr('ry', rectData.ry);
|
||||
|
||||
if (rectData.attrs !== undefined) {
|
||||
for (const attrKey in rectData.attrs) {
|
||||
rectElement.attr(attrKey, rectData.attrs[attrKey]);
|
||||
}
|
||||
}
|
||||
|
||||
rectData.class !== undefined && rectElement.attr('class', rectData.class);
|
||||
|
||||
return rectElement;
|
||||
};
|
||||
|
||||
/**
|
||||
* Draws a background rectangle
|
||||
*
|
||||
* @param element - Diagram (reference for bounds)
|
||||
* @param bounds - Shape of the rectangle
|
||||
*/
|
||||
export const drawBackgroundRect = (element: SVG | Group, bounds: Bound): void => {
|
||||
const rectData: RectData = {
|
||||
x: bounds.startx,
|
||||
y: bounds.starty,
|
||||
width: bounds.stopx - bounds.startx,
|
||||
height: bounds.stopy - bounds.starty,
|
||||
fill: bounds.fill,
|
||||
stroke: bounds.stroke,
|
||||
class: 'rect',
|
||||
};
|
||||
const rectElement: D3RectElement = drawRect(element, rectData);
|
||||
rectElement.lower();
|
||||
};
|
||||
|
||||
export const drawText = (element: SVG | Group, textData: TextData): D3TextElement => {
|
||||
const nText: string = textData.text.replace(lineBreakRegex, ' ');
|
||||
|
||||
const textElem: D3TextElement = element.append('text');
|
||||
textElem.attr('x', textData.x);
|
||||
textElem.attr('y', textData.y);
|
||||
textElem.attr('class', 'legend');
|
||||
|
||||
textElem.style('text-anchor', textData.anchor);
|
||||
textData.class !== undefined && textElem.attr('class', textData.class);
|
||||
|
||||
const tspan: D3TSpanElement = textElem.append('tspan');
|
||||
tspan.attr('x', textData.x + textData.textMargin * 2);
|
||||
tspan.text(nText);
|
||||
|
||||
return textElem;
|
||||
};
|
||||
|
||||
export const drawImage = (elem: SVG | Group, x: number, y: number, link: string): void => {
|
||||
const imageElement: D3ImageElement = elem.append('image');
|
||||
imageElement.attr('x', x);
|
||||
imageElement.attr('y', y);
|
||||
const sanitizedLink: string = sanitizeUrl(link);
|
||||
imageElement.attr('xlink:href', sanitizedLink);
|
||||
};
|
||||
|
||||
export const drawEmbeddedImage = (
|
||||
element: SVG | Group,
|
||||
x: number,
|
||||
y: number,
|
||||
link: string
|
||||
): void => {
|
||||
const imageElement: D3UseElement = element.append('use');
|
||||
imageElement.attr('x', x);
|
||||
imageElement.attr('y', y);
|
||||
const sanitizedLink: string = sanitizeUrl(link);
|
||||
imageElement.attr('xlink:href', `#${sanitizedLink}`);
|
||||
};
|
||||
|
||||
export const getNoteRect = (): RectData => {
|
||||
const noteRectData: RectData = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 100,
|
||||
height: 100,
|
||||
fill: '#EDF2AE',
|
||||
stroke: '#666',
|
||||
anchor: 'start',
|
||||
rx: 0,
|
||||
ry: 0,
|
||||
};
|
||||
return noteRectData;
|
||||
};
|
||||
|
||||
export const getTextObj = (): TextObject => {
|
||||
const testObject: TextObject = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 100,
|
||||
height: 100,
|
||||
'text-anchor': 'start',
|
||||
style: '#666',
|
||||
textMargin: 0,
|
||||
rx: 0,
|
||||
ry: 0,
|
||||
tspan: true,
|
||||
};
|
||||
return testObject;
|
||||
};
|
||||
@@ -1,5 +1,4 @@
|
||||
import { log } from '../../logger.js';
|
||||
import mermaidAPI from '../../mermaidAPI.js';
|
||||
import * as configApi from '../../config.js';
|
||||
|
||||
import {
|
||||
@@ -10,7 +9,7 @@ import {
|
||||
clear as commonClear,
|
||||
setDiagramTitle,
|
||||
getDiagramTitle,
|
||||
} from '../../commonDb.js';
|
||||
} from '../common/commonDb.js';
|
||||
|
||||
let entities = {};
|
||||
let relationships = [];
|
||||
@@ -28,14 +27,13 @@ const Identification = {
|
||||
IDENTIFYING: 'IDENTIFYING',
|
||||
};
|
||||
|
||||
export const parseDirective = function (statement, context, type) {
|
||||
mermaidAPI.parseDirective(this, statement, context, type);
|
||||
};
|
||||
|
||||
const addEntity = function (name) {
|
||||
const addEntity = function (name, alias = undefined) {
|
||||
if (entities[name] === undefined) {
|
||||
entities[name] = { attributes: [] };
|
||||
entities[name] = { attributes: [], alias: alias };
|
||||
log.info('Added new entity :', name);
|
||||
} else if (entities[name] && !entities[name].alias && alias) {
|
||||
entities[name].alias = alias;
|
||||
log.info(`Add alias '${alias}' to entity '${name}'`);
|
||||
}
|
||||
|
||||
return entities[name];
|
||||
@@ -85,7 +83,6 @@ const clear = function () {
|
||||
export default {
|
||||
Cardinality,
|
||||
Identification,
|
||||
parseDirective,
|
||||
getConfig: () => configApi.getConfig().er,
|
||||
addEntity,
|
||||
addAttributes,
|
||||
|
||||
@@ -326,7 +326,7 @@ const drawEntities = function (svgNode, entities, graph) {
|
||||
.style('text-anchor', 'middle')
|
||||
.style('font-family', getConfig().fontFamily)
|
||||
.style('font-size', conf.fontSize + 'px')
|
||||
.text(entityName);
|
||||
.text(entities[entityName].alias ?? entityName);
|
||||
|
||||
const { width: entityWidth, height: entityHeight } = drawAttributes(
|
||||
groupNode,
|
||||
@@ -555,7 +555,6 @@ const drawRelationshipFromLayout = function (svg, rel, g, insert, diagObj) {
|
||||
export const draw = function (text, id, _version, diagObj) {
|
||||
conf = getConfig().er;
|
||||
log.info('Drawing ER diagram');
|
||||
// diag.db.clear();
|
||||
const securityLevel = getConfig().securityLevel;
|
||||
// Handle root and Document for when rendering in sandbox mode
|
||||
let sandboxElement;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
%lex
|
||||
|
||||
%options case-insensitive
|
||||
%x open_directive type_directive arg_directive block
|
||||
%x block
|
||||
%x acc_title
|
||||
%x acc_descr
|
||||
%x acc_descr_multiline
|
||||
@@ -14,11 +14,6 @@ accDescr\s*":"\s* { this.begin("ac
|
||||
accDescr\s*"{"\s* { this.begin("acc_descr_multiline");}
|
||||
<acc_descr_multiline>[\}] { this.popState(); }
|
||||
<acc_descr_multiline>[^\}]* return "acc_descr_multiline_value";
|
||||
\%\%\{ { this.begin('open_directive'); return 'open_directive'; }
|
||||
<open_directive>((?:(?!\}\%\%)[^:.])*) { this.begin('type_directive'); return 'type_directive'; }
|
||||
<type_directive>":" { this.popState(); this.begin('arg_directive'); return ':'; }
|
||||
<type_directive,arg_directive>\}\%\% { this.popState(); this.popState(); return 'close_directive'; }
|
||||
<arg_directive>((?:(?!\}\%\%).|\n)*) return 'arg_directive';
|
||||
[\n]+ return 'NEWLINE';
|
||||
\s+ /* skip whitespace */
|
||||
[\s]+ return 'SPACE';
|
||||
@@ -30,11 +25,13 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multili
|
||||
<block>\s+ /* skip whitespace in block */
|
||||
<block>\b((?:PK)|(?:FK)|(?:UK))\b return 'ATTRIBUTE_KEY'
|
||||
<block>(.*?)[~](.*?)*[~] return 'ATTRIBUTE_WORD';
|
||||
<block>[A-Za-z_][A-Za-z0-9\-_\[\]\(\)]* return 'ATTRIBUTE_WORD'
|
||||
<block>[\*A-Za-z_][A-Za-z0-9\-_\[\]\(\)]* return 'ATTRIBUTE_WORD'
|
||||
<block>\"[^"]*\" return 'COMMENT';
|
||||
<block>[\n]+ /* nothing */
|
||||
<block>"}" { this.popState(); return 'BLOCK_STOP'; }
|
||||
<block>. return yytext[0];
|
||||
"[" return 'SQS';
|
||||
"]" return 'SQE';
|
||||
|
||||
"one or zero" return 'ZERO_OR_ONE';
|
||||
"one or more" return 'ONE_OR_MORE';
|
||||
@@ -64,7 +61,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';
|
||||
|
||||
@@ -75,7 +72,6 @@ o\{ return 'ZERO_OR_MORE';
|
||||
|
||||
start
|
||||
: 'ER_DIAGRAM' document 'EOF' { /*console.log('finished parsing');*/ }
|
||||
| directive start
|
||||
;
|
||||
|
||||
document
|
||||
@@ -90,29 +86,28 @@ line
|
||||
| EOF { $$=[];}
|
||||
;
|
||||
|
||||
directive
|
||||
: openDirective typeDirective closeDirective 'NEWLINE'
|
||||
| openDirective typeDirective ':' argDirective closeDirective 'NEWLINE'
|
||||
;
|
||||
|
||||
statement
|
||||
: directive
|
||||
| entityName relSpec entityName ':' role
|
||||
: entityName relSpec entityName ':' role
|
||||
{
|
||||
yy.addEntity($1);
|
||||
yy.addEntity($3);
|
||||
yy.addRelationship($1, $5, $3, $2);
|
||||
/*console.log($1 + $2 + $3 + ':' + $5);*/
|
||||
}
|
||||
| entityName BLOCK_START attributes BLOCK_STOP
|
||||
{
|
||||
/* console.log('detected block'); */
|
||||
yy.addEntity($1);
|
||||
yy.addAttributes($1, $3);
|
||||
/* console.log('handled block'); */
|
||||
}
|
||||
| entityName BLOCK_START BLOCK_STOP { yy.addEntity($1); }
|
||||
| entityName { yy.addEntity($1); }
|
||||
| entityName SQS entityName SQE BLOCK_START attributes BLOCK_STOP
|
||||
{
|
||||
yy.addEntity($1, $3);
|
||||
yy.addAttributes($1, $6);
|
||||
}
|
||||
| entityName SQS entityName SQE BLOCK_START BLOCK_STOP { yy.addEntity($1, $3); }
|
||||
| entityName SQS entityName SQE { yy.addEntity($1, $3); }
|
||||
| title title_value { $$=$2.trim();yy.setAccTitle($$); }
|
||||
| acc_title acc_title_value { $$=$2.trim();yy.setAccTitle($$); }
|
||||
| acc_descr acc_descr_value { $$=$2.trim();yy.setAccDescription($$); }
|
||||
@@ -185,20 +180,4 @@ role
|
||||
| 'ALPHANUM' { $$ = $1; }
|
||||
;
|
||||
|
||||
openDirective
|
||||
: open_directive { yy.parseDirective('%%{', 'open_directive'); }
|
||||
;
|
||||
|
||||
typeDirective
|
||||
: type_directive { yy.parseDirective($1, 'type_directive'); }
|
||||
;
|
||||
|
||||
argDirective
|
||||
: arg_directive { $1 = $1.trim().replace(/'/g, '"'); yy.parseDirective($1, 'arg_directive'); }
|
||||
;
|
||||
|
||||
closeDirective
|
||||
: close_directive { yy.parseDirective('}%%', 'close_directive', 'er'); }
|
||||
;
|
||||
|
||||
%%
|
||||
|
||||
@@ -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) => {
|
||||
@@ -133,6 +133,50 @@ describe('when parsing ER diagram it...', function () {
|
||||
const entities = erDb.getEntities();
|
||||
expect(entities.hasOwnProperty(hyphensUnderscore)).toBe(true);
|
||||
});
|
||||
|
||||
it('can have an alias', function () {
|
||||
const entity = 'foo';
|
||||
const alias = 'bar';
|
||||
erDiagram.parser.parse(`erDiagram\n${entity}["${alias}"]\n`);
|
||||
const entities = erDb.getEntities();
|
||||
expect(entities.hasOwnProperty(entity)).toBe(true);
|
||||
expect(entities[entity].alias).toBe(alias);
|
||||
});
|
||||
|
||||
it('can have an alias even if the relationship is defined before class', function () {
|
||||
const firstEntity = 'foo';
|
||||
const secondEntity = 'bar';
|
||||
const alias = 'batman';
|
||||
erDiagram.parser.parse(
|
||||
`erDiagram\n${firstEntity} ||--o| ${secondEntity} : rel\nclass ${firstEntity}["${alias}"]\n`
|
||||
);
|
||||
const entities = erDb.getEntities();
|
||||
expect(entities.hasOwnProperty(firstEntity)).toBe(true);
|
||||
expect(entities.hasOwnProperty(secondEntity)).toBe(true);
|
||||
expect(entities[firstEntity].alias).toBe(alias);
|
||||
expect(entities[secondEntity].alias).toBeUndefined();
|
||||
});
|
||||
|
||||
it('can have an alias even if the relationship is defined after class', function () {
|
||||
const firstEntity = 'foo';
|
||||
const secondEntity = 'bar';
|
||||
const alias = 'batman';
|
||||
erDiagram.parser.parse(
|
||||
`erDiagram\nclass ${firstEntity}["${alias}"]\n${firstEntity} ||--o| ${secondEntity} : rel\n`
|
||||
);
|
||||
const entities = erDb.getEntities();
|
||||
expect(entities.hasOwnProperty(firstEntity)).toBe(true);
|
||||
expect(entities.hasOwnProperty(secondEntity)).toBe(true);
|
||||
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', () => {
|
||||
@@ -154,6 +198,26 @@ describe('when parsing ER diagram it...', function () {
|
||||
expect(entities[entity].attributes[2].attributeName).toBe('author-ref[name](1)');
|
||||
});
|
||||
|
||||
it('should allow asterisk at the start of attribute name', function () {
|
||||
const entity = 'BOOK';
|
||||
const attribute = 'string *title';
|
||||
|
||||
erDiagram.parser.parse(`erDiagram\n${entity}{\n${attribute}}`);
|
||||
const entities = erDb.getEntities();
|
||||
expect(Object.keys(entities).length).toBe(1);
|
||||
expect(entities[entity].attributes.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should allow asterisks at the start of attribute declared with type and name', () => {
|
||||
const entity = 'BOOK';
|
||||
const attribute = 'id *the_Primary_Key';
|
||||
|
||||
erDiagram.parser.parse(`erDiagram\n${entity} {\n${attribute}}`);
|
||||
const entities = erDb.getEntities();
|
||||
expect(Object.keys(entities).length).toBe(1);
|
||||
expect(entities[entity].attributes.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should not allow leading numbers, dashes or brackets', function () {
|
||||
const entity = 'BOOK';
|
||||
const nonLeadingChars = '0-[]()';
|
||||
|
||||
@@ -1,23 +1,15 @@
|
||||
import { DiagramDefinition } from '../../diagram-api/types.js';
|
||||
import styles from './styles.js';
|
||||
import renderer from './errorRenderer.js';
|
||||
export const diagram: DiagramDefinition = {
|
||||
db: {
|
||||
clear: () => {
|
||||
// Quite ok, clear needs to be there for error to work as a regular diagram
|
||||
},
|
||||
},
|
||||
styles,
|
||||
import type { DiagramDefinition } from '../../diagram-api/types.js';
|
||||
import { renderer } from './errorRenderer.js';
|
||||
|
||||
const diagram: DiagramDefinition = {
|
||||
db: {},
|
||||
renderer,
|
||||
parser: {
|
||||
parser: { yy: {} },
|
||||
parse: () => {
|
||||
// no op
|
||||
parse: (): void => {
|
||||
return;
|
||||
},
|
||||
},
|
||||
init: () => {
|
||||
// no op
|
||||
},
|
||||
};
|
||||
|
||||
export default diagram;
|
||||
|
||||
@@ -1,119 +1,104 @@
|
||||
/** Created by knut on 14-12-11. */
|
||||
// @ts-ignore TODO: Investigate D3 issue
|
||||
import { select } from 'd3';
|
||||
import { log } from '../../logger.js';
|
||||
import { getErrorMessage } from '../../utils.js';
|
||||
|
||||
/**
|
||||
* Merges the value of `conf` with the passed `cnf`
|
||||
*
|
||||
* @param cnf - Config to merge
|
||||
*/
|
||||
export const setConf = function () {
|
||||
// no-op
|
||||
};
|
||||
import type { Group, SVG } from '../../diagram-api/types.js';
|
||||
import { selectSvgElement } from '../../rendering-util/selectSvgElement.js';
|
||||
import { configureSvgSize } from '../../setupGraphViewbox.js';
|
||||
import { Diagram } from '../../Diagram.js';
|
||||
|
||||
/**
|
||||
* Draws a an info picture in the tag with id: id based on the graph definition in text.
|
||||
*
|
||||
* @param _text - Mermaid graph definition.
|
||||
* @param id - The text for the error
|
||||
* @param mermaidVersion - The version
|
||||
* @param version - The version
|
||||
* @param error - The caught error
|
||||
*/
|
||||
export const draw = (
|
||||
_text: string,
|
||||
id: string,
|
||||
mermaidVersion: string,
|
||||
version: string,
|
||||
_diagramObject: Diagram,
|
||||
error: Error | null = null
|
||||
) => {
|
||||
try {
|
||||
log.debug('Renering svg for syntax error\n');
|
||||
log.debug('renering svg for syntax error\n');
|
||||
|
||||
const svg = select('#' + id);
|
||||
const svg: SVG = selectSvgElement(id);
|
||||
|
||||
const g = svg.append('g');
|
||||
if (error && error.message?.includes('KaTeX')) {
|
||||
const title = error.message.split(': ')[0];
|
||||
const body = error.message.replace(/[A-z]*:/, '').replace('KaTeX parse ', '');
|
||||
g.append('foreignObject')
|
||||
.attr('height', 100)
|
||||
.attr('width', 500)
|
||||
.append('xhtml:div')
|
||||
.style('font-size', '18px')
|
||||
.style('color', '#552222')
|
||||
.html(`<div style="font-size: 26px; margin-bottom: 8px">${title}</div><div>${body}</div>`);
|
||||
svg.attr('height', 100);
|
||||
svg.attr('width', 500);
|
||||
} else {
|
||||
g.append('path')
|
||||
.attr('class', 'error-icon')
|
||||
.attr(
|
||||
'd',
|
||||
'm411.313,123.313c6.25-6.25 6.25-16.375 0-22.625s-16.375-6.25-22.625,0l-32,32-9.375,9.375-20.688-20.688c-12.484-12.5-32.766-12.5-45.25,0l-16,16c-1.261,1.261-2.304,2.648-3.31,4.051-21.739-8.561-45.324-13.426-70.065-13.426-105.867,0-192,86.133-192,192s86.133,192 192,192 192-86.133 192-192c0-24.741-4.864-48.327-13.426-70.065 1.402-1.007 2.79-2.049 4.051-3.31l16-16c12.5-12.492 12.5-32.758 0-45.25l-20.688-20.688 9.375-9.375 32.001-31.999zm-219.313,100.687c-52.938,0-96,43.063-96,96 0,8.836-7.164,16-16,16s-16-7.164-16-16c0-70.578 57.422-128 128-128 8.836,0 16,7.164 16,16s-7.164,16-16,16z'
|
||||
);
|
||||
const g: Group = svg.append('g');
|
||||
if (error && error.message?.includes('KaTeX')) {
|
||||
const title = error.message.split(': ')[0];
|
||||
const body = error.message.replace(/[A-z]*:/, '').replace('KaTeX parse ', '');
|
||||
g.append('foreignObject')
|
||||
.attr('height', 100)
|
||||
.attr('width', 500)
|
||||
.append('xhtml:div')
|
||||
.style('font-size', '18px')
|
||||
.style('color', '#552222')
|
||||
.html(`<div style="font-size: 26px; margin-bottom: 8px">${title}</div><div>${body}</div>`);
|
||||
svg.attr('height', 100);
|
||||
svg.attr('width', 500);
|
||||
} else {
|
||||
svg.attr('viewBox', '0 0 2412 512');
|
||||
configureSvgSize(svg, 100, 512, true);
|
||||
|
||||
g.append('path')
|
||||
.attr('class', 'error-icon')
|
||||
.attr(
|
||||
'd',
|
||||
'm459.02,148.98c-6.25-6.25-16.375-6.25-22.625,0s-6.25,16.375 0,22.625l16,16c3.125,3.125 7.219,4.688 11.313,4.688 4.094,0 8.188-1.563 11.313-4.688 6.25-6.25 6.25-16.375 0-22.625l-16.001-16z'
|
||||
);
|
||||
g.append('path')
|
||||
.attr('class', 'error-icon')
|
||||
.attr(
|
||||
'd',
|
||||
'm411.313,123.313c6.25-6.25 6.25-16.375 0-22.625s-16.375-6.25-22.625,0l-32,32-9.375,9.375-20.688-20.688c-12.484-12.5-32.766-12.5-45.25,0l-16,16c-1.261,1.261-2.304,2.648-3.31,4.051-21.739-8.561-45.324-13.426-70.065-13.426-105.867,0-192,86.133-192,192s86.133,192 192,192 192-86.133 192-192c0-24.741-4.864-48.327-13.426-70.065 1.402-1.007 2.79-2.049 4.051-3.31l16-16c12.5-12.492 12.5-32.758 0-45.25l-20.688-20.688 9.375-9.375 32.001-31.999zm-219.313,100.687c-52.938,0-96,43.063-96,96 0,8.836-7.164,16-16,16s-16-7.164-16-16c0-70.578 57.422-128 128-128 8.836,0 16,7.164 16,16s-7.164,16-16,16z'
|
||||
);
|
||||
|
||||
g.append('path')
|
||||
.attr('class', 'error-icon')
|
||||
.attr(
|
||||
'd',
|
||||
'm340.395,75.605c3.125,3.125 7.219,4.688 11.313,4.688 4.094,0 8.188-1.563 11.313-4.688 6.25-6.25 6.25-16.375 0-22.625l-16-16c-6.25-6.25-16.375-6.25-22.625,0s-6.25,16.375 0,22.625l15.999,16z'
|
||||
);
|
||||
g.append('path')
|
||||
.attr('class', 'error-icon')
|
||||
.attr(
|
||||
'd',
|
||||
'm459.02,148.98c-6.25-6.25-16.375-6.25-22.625,0s-6.25,16.375 0,22.625l16,16c3.125,3.125 7.219,4.688 11.313,4.688 4.094,0 8.188-1.563 11.313-4.688 6.25-6.25 6.25-16.375 0-22.625l-16.001-16z'
|
||||
);
|
||||
|
||||
g.append('path')
|
||||
.attr('class', 'error-icon')
|
||||
.attr(
|
||||
'd',
|
||||
'm400,64c8.844,0 16-7.164 16-16v-32c0-8.836-7.156-16-16-16-8.844,0-16,7.164-16,16v32c0,8.836 7.156,16 16,16z'
|
||||
);
|
||||
g.append('path')
|
||||
.attr('class', 'error-icon')
|
||||
.attr(
|
||||
'd',
|
||||
'm340.395,75.605c3.125,3.125 7.219,4.688 11.313,4.688 4.094,0 8.188-1.563 11.313-4.688 6.25-6.25 6.25-16.375 0-22.625l-16-16c-6.25-6.25-16.375-6.25-22.625,0s-6.25,16.375 0,22.625l15.999,16z'
|
||||
);
|
||||
|
||||
g.append('path')
|
||||
.attr('class', 'error-icon')
|
||||
.attr(
|
||||
'd',
|
||||
'm496,96.586h-32c-8.844,0-16,7.164-16,16 0,8.836 7.156,16 16,16h32c8.844,0 16-7.164 16-16 0-8.836-7.156-16-16-16z'
|
||||
);
|
||||
g.append('path')
|
||||
.attr('class', 'error-icon')
|
||||
.attr(
|
||||
'd',
|
||||
'm400,64c8.844,0 16-7.164 16-16v-32c0-8.836-7.156-16-16-16-8.844,0-16,7.164-16,16v32c0,8.836 7.156,16 16,16z'
|
||||
);
|
||||
|
||||
g.append('path')
|
||||
.attr('class', 'error-icon')
|
||||
.attr(
|
||||
'd',
|
||||
'm436.98,75.605c3.125,3.125 7.219,4.688 11.313,4.688 4.094,0 8.188-1.563 11.313-4.688l32-32c6.25-6.25 6.25-16.375 0-22.625s-16.375-6.25-22.625,0l-32,32c-6.251,6.25-6.251,16.375-0.001,22.625z'
|
||||
);
|
||||
g.append('path')
|
||||
.attr('class', 'error-icon')
|
||||
.attr(
|
||||
'd',
|
||||
'm496,96.586h-32c-8.844,0-16,7.164-16,16 0,8.836 7.156,16 16,16h32c8.844,0 16-7.164 16-16 0-8.836-7.156-16-16-16z'
|
||||
);
|
||||
|
||||
g.append('text') // text label for the x axis
|
||||
.attr('class', 'error-text')
|
||||
.attr('x', 1440)
|
||||
.attr('y', 250)
|
||||
.attr('font-size', '150px')
|
||||
.style('text-anchor', 'middle')
|
||||
.text('Syntax error in graph');
|
||||
g.append('text') // text label for the x axis
|
||||
.attr('class', 'error-text')
|
||||
.attr('x', 1250)
|
||||
.attr('y', 400)
|
||||
.attr('font-size', '100px')
|
||||
.style('text-anchor', 'middle')
|
||||
.text('mermaid version ' + mermaidVersion);
|
||||
g.append('path')
|
||||
.attr('class', 'error-icon')
|
||||
.attr(
|
||||
'd',
|
||||
'm436.98,75.605c3.125,3.125 7.219,4.688 11.313,4.688 4.094,0 8.188-1.563 11.313-4.688l32-32c6.25-6.25 6.25-16.375 0-22.625s-16.375-6.25-22.625,0l-32,32c-6.251,6.25-6.251,16.375-0.001,22.625z'
|
||||
);
|
||||
|
||||
svg.attr('height', 100);
|
||||
svg.attr('width', 500);
|
||||
svg.attr('viewBox', '768 0 912 512');
|
||||
g.append('text') // text label for the x axis
|
||||
.attr('class', 'error-text')
|
||||
.attr('x', 1440)
|
||||
.attr('y', 250)
|
||||
.attr('font-size', '150px')
|
||||
.style('text-anchor', 'middle')
|
||||
.text('Syntax error in text');
|
||||
g.append('text') // text label for the x axis
|
||||
.attr('class', 'error-text')
|
||||
.attr('x', 1250)
|
||||
.attr('y', 400)
|
||||
.attr('font-size', '100px')
|
||||
.style('text-anchor', 'middle')
|
||||
.text(`mermaid version ${version}`);
|
||||
}
|
||||
} catch (e) {
|
||||
log.error('Error while rendering info diagram');
|
||||
log.error(getErrorMessage(e));
|
||||
}
|
||||
};
|
||||
|
||||
export default {
|
||||
setConf,
|
||||
draw,
|
||||
};
|
||||
export const renderer = { draw };
|
||||
|
||||
export default renderer;
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
const getStyles = () => ``;
|
||||
|
||||
export default getStyles;
|
||||
@@ -4,13 +4,14 @@ import insertMarkers from '../../../dagre-wrapper/markers.js';
|
||||
import { insertEdgeLabel } from '../../../dagre-wrapper/edges.js';
|
||||
import { findCommonAncestor } from './render-utils.js';
|
||||
import { labelHelper } from '../../../dagre-wrapper/shapes/util.js';
|
||||
import { addHtmlLabel } from 'dagre-d3-es/src/dagre-js/label/add-html-label.js';
|
||||
import { getConfig } from '../../../config.js';
|
||||
import { log } from '../../../logger.js';
|
||||
import { setupGraphViewbox } from '../../../setupGraphViewbox.js';
|
||||
import common, { evaluate } from '../../common/common.js';
|
||||
import common from '../../common/common.js';
|
||||
import { interpolateToCurve, getStylesFromArray } from '../../../utils.js';
|
||||
import ELK from 'elkjs/lib/elk.bundled.js';
|
||||
import { getLineFunctionsWithOffset } from '../../../utils/lineWithOffset.js';
|
||||
|
||||
const elk = new ELK();
|
||||
|
||||
let portPos = {};
|
||||
@@ -568,8 +569,9 @@ export const addEdges = function (edges, diagObj, graph, svg) {
|
||||
* @param edgeData
|
||||
* @param diagramType
|
||||
* @param arrowMarkerAbsolute
|
||||
* @param id
|
||||
*/
|
||||
const addMarkersToEdge = function (svgPath, edgeData, diagramType, arrowMarkerAbsolute) {
|
||||
const addMarkersToEdge = function (svgPath, edgeData, diagramType, arrowMarkerAbsolute, id) {
|
||||
let url = '';
|
||||
// Check configuration for absolute path
|
||||
if (arrowMarkerAbsolute) {
|
||||
@@ -586,61 +588,103 @@ const addMarkersToEdge = function (svgPath, edgeData, diagramType, arrowMarkerAb
|
||||
// look in edge data and decide which marker to use
|
||||
switch (edgeData.arrowTypeStart) {
|
||||
case 'arrow_cross':
|
||||
svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-crossStart' + ')');
|
||||
svgPath.attr(
|
||||
'marker-start',
|
||||
'url(' + url + '#' + id + '_' + diagramType + '-crossStart' + ')'
|
||||
);
|
||||
break;
|
||||
case 'arrow_point':
|
||||
svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-pointStart' + ')');
|
||||
svgPath.attr(
|
||||
'marker-start',
|
||||
'url(' + url + '#' + id + '_' + diagramType + '-pointStart' + ')'
|
||||
);
|
||||
break;
|
||||
case 'arrow_barb':
|
||||
svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-barbStart' + ')');
|
||||
svgPath.attr(
|
||||
'marker-start',
|
||||
'url(' + url + '#' + id + '_' + diagramType + '-barbStart' + ')'
|
||||
);
|
||||
break;
|
||||
case 'arrow_circle':
|
||||
svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-circleStart' + ')');
|
||||
svgPath.attr(
|
||||
'marker-start',
|
||||
'url(' + url + '#' + id + '_' + diagramType + '-circleStart' + ')'
|
||||
);
|
||||
break;
|
||||
case 'aggregation':
|
||||
svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-aggregationStart' + ')');
|
||||
svgPath.attr(
|
||||
'marker-start',
|
||||
'url(' + url + '#' + id + '_' + diagramType + '-aggregationStart' + ')'
|
||||
);
|
||||
break;
|
||||
case 'extension':
|
||||
svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-extensionStart' + ')');
|
||||
svgPath.attr(
|
||||
'marker-start',
|
||||
'url(' + url + '#' + id + '_' + diagramType + '-extensionStart' + ')'
|
||||
);
|
||||
break;
|
||||
case 'composition':
|
||||
svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-compositionStart' + ')');
|
||||
svgPath.attr(
|
||||
'marker-start',
|
||||
'url(' + url + '#' + id + '_' + diagramType + '-compositionStart' + ')'
|
||||
);
|
||||
break;
|
||||
case 'dependency':
|
||||
svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-dependencyStart' + ')');
|
||||
svgPath.attr(
|
||||
'marker-start',
|
||||
'url(' + url + '#' + id + '_' + diagramType + '-dependencyStart' + ')'
|
||||
);
|
||||
break;
|
||||
case 'lollipop':
|
||||
svgPath.attr('marker-start', 'url(' + url + '#' + diagramType + '-lollipopStart' + ')');
|
||||
svgPath.attr(
|
||||
'marker-start',
|
||||
'url(' + url + '#' + id + '_' + diagramType + '-lollipopStart' + ')'
|
||||
);
|
||||
break;
|
||||
default:
|
||||
}
|
||||
switch (edgeData.arrowTypeEnd) {
|
||||
case 'arrow_cross':
|
||||
svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-crossEnd' + ')');
|
||||
svgPath.attr('marker-end', 'url(' + url + '#' + id + '_' + diagramType + '-crossEnd' + ')');
|
||||
break;
|
||||
case 'arrow_point':
|
||||
svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-pointEnd' + ')');
|
||||
svgPath.attr('marker-end', 'url(' + url + '#' + id + '_' + diagramType + '-pointEnd' + ')');
|
||||
break;
|
||||
case 'arrow_barb':
|
||||
svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-barbEnd' + ')');
|
||||
svgPath.attr('marker-end', 'url(' + url + '#' + id + '_' + diagramType + '-barbEnd' + ')');
|
||||
break;
|
||||
case 'arrow_circle':
|
||||
svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-circleEnd' + ')');
|
||||
svgPath.attr('marker-end', 'url(' + url + '#' + id + '_' + diagramType + '-circleEnd' + ')');
|
||||
break;
|
||||
case 'aggregation':
|
||||
svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-aggregationEnd' + ')');
|
||||
svgPath.attr(
|
||||
'marker-end',
|
||||
'url(' + url + '#' + id + '_' + diagramType + '-aggregationEnd' + ')'
|
||||
);
|
||||
break;
|
||||
case 'extension':
|
||||
svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-extensionEnd' + ')');
|
||||
svgPath.attr(
|
||||
'marker-end',
|
||||
'url(' + url + '#' + id + '_' + diagramType + '-extensionEnd' + ')'
|
||||
);
|
||||
break;
|
||||
case 'composition':
|
||||
svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-compositionEnd' + ')');
|
||||
svgPath.attr(
|
||||
'marker-end',
|
||||
'url(' + url + '#' + id + '_' + diagramType + '-compositionEnd' + ')'
|
||||
);
|
||||
break;
|
||||
case 'dependency':
|
||||
svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-dependencyEnd' + ')');
|
||||
svgPath.attr(
|
||||
'marker-end',
|
||||
'url(' + url + '#' + id + '_' + diagramType + '-dependencyEnd' + ')'
|
||||
);
|
||||
break;
|
||||
case 'lollipop':
|
||||
svgPath.attr('marker-end', 'url(' + url + '#' + diagramType + '-lollipopEnd' + ')');
|
||||
svgPath.attr(
|
||||
'marker-end',
|
||||
'url(' + url + '#' + id + '_' + diagramType + '-lollipopEnd' + ')'
|
||||
);
|
||||
break;
|
||||
default:
|
||||
}
|
||||
@@ -651,18 +695,11 @@ const addMarkersToEdge = function (svgPath, edgeData, diagramType, arrowMarkerAb
|
||||
*
|
||||
* @param text
|
||||
* @param diagObj
|
||||
* @returns {object} ClassDef styles
|
||||
* @returns {Record<string, import('../../../diagram-api/types.js').DiagramStyleClassDef>} ClassDef styles
|
||||
*/
|
||||
export const getClasses = function (text, diagObj) {
|
||||
log.info('Extracting classes');
|
||||
diagObj.db.clear('ver-2');
|
||||
try {
|
||||
// Parse the graph definition
|
||||
diagObj.parse(text);
|
||||
return diagObj.db.getClasses();
|
||||
} catch (e) {
|
||||
return {};
|
||||
}
|
||||
return diagObj.db.getClasses();
|
||||
};
|
||||
|
||||
const addSubGraphs = function (db) {
|
||||
@@ -698,7 +735,7 @@ const calcOffset = function (src, dest, parentLookupDb) {
|
||||
return { x: ancestorOffset.posX, y: ancestorOffset.posY };
|
||||
};
|
||||
|
||||
const insertEdge = function (edgesEl, edge, edgeData, diagObj, parentLookupDb) {
|
||||
const insertEdge = function (edgesEl, edge, edgeData, diagObj, parentLookupDb, id) {
|
||||
const offset = calcOffset(edge.sourceId, edge.targetId, parentLookupDb);
|
||||
|
||||
const src = edge.sections[0].startPoint;
|
||||
@@ -712,8 +749,8 @@ const insertEdge = function (edgesEl, edge, edgeData, diagObj, parentLookupDb) {
|
||||
[dest.x + offset.x, dest.y + offset.y],
|
||||
];
|
||||
|
||||
// const curve = line().curve(curveBasis);
|
||||
const curve = line().curve(curveLinear);
|
||||
const { x, y } = getLineFunctionsWithOffset(edge.edgeData);
|
||||
const curve = line().x(x).y(y).curve(curveLinear);
|
||||
const edgePath = edgesEl
|
||||
.insert('path')
|
||||
.attr('d', curve(points))
|
||||
@@ -729,7 +766,7 @@ const insertEdge = function (edgesEl, edge, edgeData, diagObj, parentLookupDb) {
|
||||
'transform',
|
||||
`translate(${edge.labels[0].x + offset.x}, ${edge.labels[0].y + offset.y})`
|
||||
);
|
||||
addMarkersToEdge(edgePath, edgeData, diagObj.type, diagObj.arrowMarkerAbsolute);
|
||||
addMarkersToEdge(edgePath, edgeData, diagObj.type, diagObj.arrowMarkerAbsolute, id);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -766,14 +803,8 @@ const insertChildren = (nodeArray, parentLookupDb) => {
|
||||
*/
|
||||
|
||||
export const draw = async function (text, id, _version, diagObj) {
|
||||
// Add temporary render element
|
||||
diagObj.db.clear();
|
||||
nodeDb = {};
|
||||
portPos = {};
|
||||
diagObj.db.setGen('gen-2');
|
||||
// Parse the graph definition
|
||||
diagObj.parser.parse(text);
|
||||
|
||||
const renderEl = select('body').append('div').attr('style', 'height:400px').attr('id', 'cy');
|
||||
let graph = {
|
||||
id: 'root',
|
||||
@@ -828,7 +859,7 @@ export const draw = async function (text, id, _version, diagObj) {
|
||||
const markers = ['point', 'circle', 'cross'];
|
||||
|
||||
// Add the marker definitions to the svg as marker tags
|
||||
insertMarkers(svg, markers, diagObj.type, diagObj.arrowMarkerAbsolute);
|
||||
insertMarkers(svg, markers, diagObj.type, id);
|
||||
|
||||
// Fetch the vertices/nodes and edges/links from the parsed graph definition
|
||||
const vert = diagObj.db.getVertices();
|
||||
@@ -907,7 +938,7 @@ export const draw = async function (text, id, _version, diagObj) {
|
||||
drawNodes(0, 0, g.children, svg, subGraphsEl, diagObj, 0);
|
||||
log.info('after layout', g);
|
||||
g.edges?.map((edge) => {
|
||||
insertEdge(edgesEl, edge, edge.edgeData, diagObj, parentLookupDb);
|
||||
insertEdge(edgesEl, edge, edge.edgeData, diagObj, parentLookupDb, id);
|
||||
});
|
||||
setupGraphViewbox({}, svg, conf.diagramPadding, conf.useMaxWidth);
|
||||
// Remove element after layout
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { findCommonAncestor, TreeData } from './render-utils.js';
|
||||
import type { TreeData } from './render-utils.js';
|
||||
import { findCommonAncestor } from './render-utils.js';
|
||||
describe('when rendering a flowchart using elk ', () => {
|
||||
let lookupDb: TreeData;
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -2,7 +2,6 @@ import { select } from 'd3';
|
||||
import utils from '../../utils.js';
|
||||
import * as configApi from '../../config.js';
|
||||
import common from '../common/common.js';
|
||||
import mermaidAPI from '../../mermaidAPI.js';
|
||||
import { log } from '../../logger.js';
|
||||
import {
|
||||
setAccTitle,
|
||||
@@ -12,7 +11,7 @@ import {
|
||||
clear as commonClear,
|
||||
setDiagramTitle,
|
||||
getDiagramTitle,
|
||||
} from '../../commonDb.js';
|
||||
} from '../common/commonDb.js';
|
||||
|
||||
const MERMAID_DOM_ID_PREFIX = 'flowchart-';
|
||||
let vertexCounter = 0;
|
||||
@@ -34,10 +33,6 @@ let funs = [];
|
||||
|
||||
const sanitizeText = (txt) => common.sanitizeText(txt, config);
|
||||
|
||||
export const parseDirective = function (statement, context, type) {
|
||||
mermaidAPI.parseDirective(this, statement, context, type);
|
||||
};
|
||||
|
||||
/**
|
||||
* Function to lookup domId from id in the graph definition.
|
||||
*
|
||||
@@ -771,7 +766,6 @@ export const lex = {
|
||||
firstGraph,
|
||||
};
|
||||
export default {
|
||||
parseDirective,
|
||||
defaultConfig: () => configApi.defaultConfig.flowchart,
|
||||
setAccTitle,
|
||||
getAccTitle,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// @ts-ignore: TODO Fix ts errors
|
||||
// @ts-ignore: JISON doesn't support types
|
||||
import flowParser from './parser/flow.jison';
|
||||
import flowDb from './flowDb.js';
|
||||
import flowRendererV2 from './flowRenderer-v2.js';
|
||||
import flowStyles from './styles.js';
|
||||
import { MermaidConfig } from '../../config.type.js';
|
||||
import type { MermaidConfig } from '../../config.type.js';
|
||||
import { setConfig } from '../../config.js';
|
||||
|
||||
export const diagram = {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// @ts-ignore: TODO Fix ts errors
|
||||
// @ts-ignore: JISON doesn't support types
|
||||
import flowParser from './parser/flow.jison';
|
||||
import flowDb from './flowDb.js';
|
||||
import flowRenderer from './flowRenderer.js';
|
||||
import flowRendererV2 from './flowRenderer-v2.js';
|
||||
import flowStyles from './styles.js';
|
||||
import { MermaidConfig } from '../../config.type.js';
|
||||
import type { MermaidConfig } from '../../config.type.js';
|
||||
|
||||
export const diagram = {
|
||||
parser: flowParser,
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import * as graphlib from 'dagre-d3-es/src/graphlib/index.js';
|
||||
import { select, curveLinear, selectAll } from 'd3';
|
||||
|
||||
import flowDb from './flowDb.js';
|
||||
import { getConfig } from '../../config.js';
|
||||
import utils from '../../utils.js';
|
||||
|
||||
import { render } from '../../dagre-wrapper/index.js';
|
||||
import { addHtmlLabel } from 'dagre-d3-es/src/dagre-js/label/add-html-label.js';
|
||||
import { log } from '../../logger.js';
|
||||
@@ -339,18 +336,10 @@ export const addEdges = async function (edges, g, diagObj) {
|
||||
*
|
||||
* @param text
|
||||
* @param diagObj
|
||||
* @returns {object} ClassDef styles
|
||||
* @returns {Record<string, import('../../diagram-api/types.js').DiagramStyleClassDef>} ClassDef styles
|
||||
*/
|
||||
export const getClasses = function (text, diagObj) {
|
||||
log.info('Extracting classes');
|
||||
diagObj.db.clear();
|
||||
try {
|
||||
// Parse the graph definition
|
||||
diagObj.parse(text);
|
||||
return diagObj.db.getClasses();
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
return diagObj.db.getClasses();
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -362,10 +351,6 @@ export const getClasses = function (text, diagObj) {
|
||||
|
||||
export const draw = async function (text, id, _version, diagObj) {
|
||||
log.info('Drawing flowchart');
|
||||
diagObj.db.clear();
|
||||
flowDb.setGen('gen-2');
|
||||
// Parse the graph definition
|
||||
diagObj.parser.parse(text);
|
||||
|
||||
// Fetch the default direction, use TD if none was found
|
||||
let dir = diagObj.db.getDirection();
|
||||
|
||||
@@ -275,19 +275,11 @@ export const addEdges = async function (edges, g, diagObj) {
|
||||
*
|
||||
* @param text
|
||||
* @param diagObj
|
||||
* @returns {object} ClassDef styles
|
||||
* @returns {Record<string, import('../../diagram-api/types.js').DiagramStyleClassDef>} ClassDef styles
|
||||
*/
|
||||
export const getClasses = function (text, diagObj) {
|
||||
log.info('Extracting classes');
|
||||
diagObj.db.clear();
|
||||
try {
|
||||
// Parse the graph definition
|
||||
diagObj.parse(text);
|
||||
return diagObj.db.getClasses();
|
||||
} catch (e) {
|
||||
log.error(e);
|
||||
return {};
|
||||
}
|
||||
return diagObj.db.getClasses();
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -300,7 +292,6 @@ export const getClasses = function (text, diagObj) {
|
||||
*/
|
||||
export const draw = async function (text, id, _version, diagObj) {
|
||||
log.info('Drawing flowchart');
|
||||
diagObj.db.clear();
|
||||
const { securityLevel, flowchart: conf } = getConfig();
|
||||
let sandboxElement;
|
||||
if (securityLevel === 'sandbox') {
|
||||
|
||||
@@ -6,6 +6,40 @@ setConfig({
|
||||
securityLevel: 'strict',
|
||||
});
|
||||
|
||||
const keywords = [
|
||||
'graph',
|
||||
'flowchart',
|
||||
'flowchart-elk',
|
||||
'style',
|
||||
'default',
|
||||
'linkStyle',
|
||||
'interpolate',
|
||||
'classDef',
|
||||
'class',
|
||||
'href',
|
||||
'call',
|
||||
'click',
|
||||
'_self',
|
||||
'_blank',
|
||||
'_parent',
|
||||
'_top',
|
||||
'end',
|
||||
'subgraph',
|
||||
'kitty',
|
||||
];
|
||||
|
||||
const doubleEndedEdges = [
|
||||
{ edgeStart: 'x--', edgeEnd: '--x', stroke: 'normal', type: 'double_arrow_cross' },
|
||||
{ edgeStart: 'x==', edgeEnd: '==x', stroke: 'thick', type: 'double_arrow_cross' },
|
||||
{ edgeStart: 'x-.', edgeEnd: '.-x', stroke: 'dotted', type: 'double_arrow_cross' },
|
||||
{ edgeStart: 'o--', edgeEnd: '--o', stroke: 'normal', type: 'double_arrow_circle' },
|
||||
{ edgeStart: 'o==', edgeEnd: '==o', stroke: 'thick', type: 'double_arrow_circle' },
|
||||
{ edgeStart: 'o-.', edgeEnd: '.-o', stroke: 'dotted', type: 'double_arrow_circle' },
|
||||
{ edgeStart: '<--', edgeEnd: '-->', stroke: 'normal', type: 'double_arrow_point' },
|
||||
{ edgeStart: '<==', edgeEnd: '==>', stroke: 'thick', type: 'double_arrow_point' },
|
||||
{ edgeStart: '<-.', edgeEnd: '.->', stroke: 'dotted', type: 'double_arrow_point' },
|
||||
];
|
||||
|
||||
describe('[Edges] when parsing', () => {
|
||||
beforeEach(function () {
|
||||
flow.parser.yy = flowDb;
|
||||
@@ -39,211 +73,62 @@ describe('[Edges] when parsing', () => {
|
||||
expect(edges[0].type).toBe('arrow_circle');
|
||||
});
|
||||
|
||||
describe('cross', function () {
|
||||
it('should handle double edged nodes and edges', function () {
|
||||
const res = flow.parser.parse('graph TD;\nA x--x B;');
|
||||
describe('edges', function () {
|
||||
doubleEndedEdges.forEach((edgeType) => {
|
||||
it(`should handle ${edgeType.stroke} ${edgeType.type} with no text`, function () {
|
||||
const res = flow.parser.parse(`graph TD;\nA ${edgeType.edgeStart}${edgeType.edgeEnd} B;`);
|
||||
|
||||
const vert = flow.parser.yy.getVertices();
|
||||
const edges = flow.parser.yy.getEdges();
|
||||
const vert = flow.parser.yy.getVertices();
|
||||
const edges = flow.parser.yy.getEdges();
|
||||
|
||||
expect(vert['A'].id).toBe('A');
|
||||
expect(vert['B'].id).toBe('B');
|
||||
expect(edges.length).toBe(1);
|
||||
expect(edges[0].start).toBe('A');
|
||||
expect(edges[0].end).toBe('B');
|
||||
expect(edges[0].type).toBe('double_arrow_cross');
|
||||
expect(edges[0].text).toBe('');
|
||||
expect(edges[0].stroke).toBe('normal');
|
||||
expect(edges[0].length).toBe(1);
|
||||
});
|
||||
expect(vert['A'].id).toBe('A');
|
||||
expect(vert['B'].id).toBe('B');
|
||||
expect(edges.length).toBe(1);
|
||||
expect(edges[0].start).toBe('A');
|
||||
expect(edges[0].end).toBe('B');
|
||||
expect(edges[0].type).toBe(`${edgeType.type}`);
|
||||
expect(edges[0].text).toBe('');
|
||||
expect(edges[0].stroke).toBe(`${edgeType.stroke}`);
|
||||
});
|
||||
|
||||
it('should handle double edged nodes with text', function () {
|
||||
const res = flow.parser.parse('graph TD;\nA x-- text --x B;');
|
||||
it(`should handle ${edgeType.stroke} ${edgeType.type} with text`, function () {
|
||||
const res = flow.parser.parse(
|
||||
`graph TD;\nA ${edgeType.edgeStart} text ${edgeType.edgeEnd} B;`
|
||||
);
|
||||
|
||||
const vert = flow.parser.yy.getVertices();
|
||||
const edges = flow.parser.yy.getEdges();
|
||||
const vert = flow.parser.yy.getVertices();
|
||||
const edges = flow.parser.yy.getEdges();
|
||||
|
||||
expect(vert['A'].id).toBe('A');
|
||||
expect(vert['B'].id).toBe('B');
|
||||
expect(edges.length).toBe(1);
|
||||
expect(edges[0].start).toBe('A');
|
||||
expect(edges[0].end).toBe('B');
|
||||
expect(edges[0].type).toBe('double_arrow_cross');
|
||||
expect(edges[0].text).toBe('text');
|
||||
expect(edges[0].stroke).toBe('normal');
|
||||
expect(edges[0].length).toBe(1);
|
||||
});
|
||||
expect(vert['A'].id).toBe('A');
|
||||
expect(vert['B'].id).toBe('B');
|
||||
expect(edges.length).toBe(1);
|
||||
expect(edges[0].start).toBe('A');
|
||||
expect(edges[0].end).toBe('B');
|
||||
expect(edges[0].type).toBe(`${edgeType.type}`);
|
||||
expect(edges[0].text).toBe('text');
|
||||
expect(edges[0].stroke).toBe(`${edgeType.stroke}`);
|
||||
});
|
||||
|
||||
it('should handle double edged nodes and edges on thick arrows', function () {
|
||||
const res = flow.parser.parse('graph TD;\nA x==x B;');
|
||||
it.each(keywords)(
|
||||
`should handle ${edgeType.stroke} ${edgeType.type} with %s text`,
|
||||
function (keyword) {
|
||||
const res = flow.parser.parse(
|
||||
`graph TD;\nA ${edgeType.edgeStart} ${keyword} ${edgeType.edgeEnd} B;`
|
||||
);
|
||||
|
||||
const vert = flow.parser.yy.getVertices();
|
||||
const edges = flow.parser.yy.getEdges();
|
||||
const vert = flow.parser.yy.getVertices();
|
||||
const edges = flow.parser.yy.getEdges();
|
||||
|
||||
expect(vert['A'].id).toBe('A');
|
||||
expect(vert['B'].id).toBe('B');
|
||||
expect(edges.length).toBe(1);
|
||||
expect(edges[0].start).toBe('A');
|
||||
expect(edges[0].end).toBe('B');
|
||||
expect(edges[0].type).toBe('double_arrow_cross');
|
||||
expect(edges[0].text).toBe('');
|
||||
expect(edges[0].stroke).toBe('thick');
|
||||
expect(edges[0].length).toBe(1);
|
||||
});
|
||||
|
||||
it('should handle double edged nodes with text on thick arrows', function () {
|
||||
const res = flow.parser.parse('graph TD;\nA x== text ==x B;');
|
||||
|
||||
const vert = flow.parser.yy.getVertices();
|
||||
const edges = flow.parser.yy.getEdges();
|
||||
|
||||
expect(vert['A'].id).toBe('A');
|
||||
expect(vert['B'].id).toBe('B');
|
||||
expect(edges.length).toBe(1);
|
||||
expect(edges[0].start).toBe('A');
|
||||
expect(edges[0].end).toBe('B');
|
||||
expect(edges[0].type).toBe('double_arrow_cross');
|
||||
expect(edges[0].text).toBe('text');
|
||||
expect(edges[0].stroke).toBe('thick');
|
||||
expect(edges[0].length).toBe(1);
|
||||
});
|
||||
|
||||
it('should handle double edged nodes and edges on dotted arrows', function () {
|
||||
const res = flow.parser.parse('graph TD;\nA x-.-x B;');
|
||||
|
||||
const vert = flow.parser.yy.getVertices();
|
||||
const edges = flow.parser.yy.getEdges();
|
||||
|
||||
expect(vert['A'].id).toBe('A');
|
||||
expect(vert['B'].id).toBe('B');
|
||||
expect(edges.length).toBe(1);
|
||||
expect(edges[0].start).toBe('A');
|
||||
expect(edges[0].end).toBe('B');
|
||||
expect(edges[0].type).toBe('double_arrow_cross');
|
||||
expect(edges[0].text).toBe('');
|
||||
expect(edges[0].stroke).toBe('dotted');
|
||||
expect(edges[0].length).toBe(1);
|
||||
});
|
||||
|
||||
it('should handle double edged nodes with text on dotted arrows', function () {
|
||||
const res = flow.parser.parse('graph TD;\nA x-. text .-x B;');
|
||||
|
||||
const vert = flow.parser.yy.getVertices();
|
||||
const edges = flow.parser.yy.getEdges();
|
||||
|
||||
expect(vert['A'].id).toBe('A');
|
||||
expect(vert['B'].id).toBe('B');
|
||||
expect(edges.length).toBe(1);
|
||||
expect(edges[0].start).toBe('A');
|
||||
expect(edges[0].end).toBe('B');
|
||||
expect(edges[0].type).toBe('double_arrow_cross');
|
||||
expect(edges[0].text).toBe('text');
|
||||
expect(edges[0].stroke).toBe('dotted');
|
||||
expect(edges[0].length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('circle', function () {
|
||||
it('should handle double edged nodes and edges', function () {
|
||||
const res = flow.parser.parse('graph TD;\nA o--o B;');
|
||||
|
||||
const vert = flow.parser.yy.getVertices();
|
||||
const edges = flow.parser.yy.getEdges();
|
||||
|
||||
expect(vert['A'].id).toBe('A');
|
||||
expect(vert['B'].id).toBe('B');
|
||||
expect(edges.length).toBe(1);
|
||||
expect(edges[0].start).toBe('A');
|
||||
expect(edges[0].end).toBe('B');
|
||||
expect(edges[0].type).toBe('double_arrow_circle');
|
||||
expect(edges[0].text).toBe('');
|
||||
expect(edges[0].stroke).toBe('normal');
|
||||
expect(edges[0].length).toBe(1);
|
||||
});
|
||||
|
||||
it('should handle double edged nodes with text', function () {
|
||||
const res = flow.parser.parse('graph TD;\nA o-- text --o B;');
|
||||
|
||||
const vert = flow.parser.yy.getVertices();
|
||||
const edges = flow.parser.yy.getEdges();
|
||||
|
||||
expect(vert['A'].id).toBe('A');
|
||||
expect(vert['B'].id).toBe('B');
|
||||
expect(edges.length).toBe(1);
|
||||
expect(edges[0].start).toBe('A');
|
||||
expect(edges[0].end).toBe('B');
|
||||
expect(edges[0].type).toBe('double_arrow_circle');
|
||||
expect(edges[0].text).toBe('text');
|
||||
expect(edges[0].stroke).toBe('normal');
|
||||
expect(edges[0].length).toBe(1);
|
||||
});
|
||||
|
||||
it('should handle double edged nodes and edges on thick arrows', function () {
|
||||
const res = flow.parser.parse('graph TD;\nA o==o B;');
|
||||
|
||||
const vert = flow.parser.yy.getVertices();
|
||||
const edges = flow.parser.yy.getEdges();
|
||||
|
||||
expect(vert['A'].id).toBe('A');
|
||||
expect(vert['B'].id).toBe('B');
|
||||
expect(edges.length).toBe(1);
|
||||
expect(edges[0].start).toBe('A');
|
||||
expect(edges[0].end).toBe('B');
|
||||
expect(edges[0].type).toBe('double_arrow_circle');
|
||||
expect(edges[0].text).toBe('');
|
||||
expect(edges[0].stroke).toBe('thick');
|
||||
expect(edges[0].length).toBe(1);
|
||||
});
|
||||
|
||||
it('should handle double edged nodes with text on thick arrows', function () {
|
||||
const res = flow.parser.parse('graph TD;\nA o== text ==o B;');
|
||||
|
||||
const vert = flow.parser.yy.getVertices();
|
||||
const edges = flow.parser.yy.getEdges();
|
||||
|
||||
expect(vert['A'].id).toBe('A');
|
||||
expect(vert['B'].id).toBe('B');
|
||||
expect(edges.length).toBe(1);
|
||||
expect(edges[0].start).toBe('A');
|
||||
expect(edges[0].end).toBe('B');
|
||||
expect(edges[0].type).toBe('double_arrow_circle');
|
||||
expect(edges[0].text).toBe('text');
|
||||
expect(edges[0].stroke).toBe('thick');
|
||||
expect(edges[0].length).toBe(1);
|
||||
});
|
||||
|
||||
it('should handle double edged nodes and edges on dotted arrows', function () {
|
||||
const res = flow.parser.parse('graph TD;\nA o-.-o B;');
|
||||
|
||||
const vert = flow.parser.yy.getVertices();
|
||||
const edges = flow.parser.yy.getEdges();
|
||||
|
||||
expect(vert['A'].id).toBe('A');
|
||||
expect(vert['B'].id).toBe('B');
|
||||
expect(edges.length).toBe(1);
|
||||
expect(edges[0].start).toBe('A');
|
||||
expect(edges[0].end).toBe('B');
|
||||
expect(edges[0].type).toBe('double_arrow_circle');
|
||||
expect(edges[0].text).toBe('');
|
||||
expect(edges[0].stroke).toBe('dotted');
|
||||
expect(edges[0].length).toBe(1);
|
||||
});
|
||||
|
||||
it('should handle double edged nodes with text on dotted arrows', function () {
|
||||
const res = flow.parser.parse('graph TD;\nA o-. text .-o B;');
|
||||
|
||||
const vert = flow.parser.yy.getVertices();
|
||||
const edges = flow.parser.yy.getEdges();
|
||||
|
||||
expect(vert['A'].id).toBe('A');
|
||||
expect(vert['B'].id).toBe('B');
|
||||
expect(edges.length).toBe(1);
|
||||
expect(edges[0].start).toBe('A');
|
||||
expect(edges[0].end).toBe('B');
|
||||
expect(edges[0].type).toBe('double_arrow_circle');
|
||||
expect(edges[0].text).toBe('text');
|
||||
expect(edges[0].stroke).toBe('dotted');
|
||||
expect(edges[0].length).toBe(1);
|
||||
expect(vert['A'].id).toBe('A');
|
||||
expect(vert['B'].id).toBe('B');
|
||||
expect(edges.length).toBe(1);
|
||||
expect(edges[0].start).toBe('A');
|
||||
expect(edges[0].end).toBe('B');
|
||||
expect(edges[0].type).toBe(`${edgeType.type}`);
|
||||
expect(edges[0].text).toBe(`${keyword}`);
|
||||
expect(edges[0].stroke).toBe(`${edgeType.stroke}`);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -24,7 +24,7 @@ A["\`The cat in **the** hat\`"]-- "\`The *bat* in the chat\`" -->B["The dog in t
|
||||
expect(vert['A'].labelType).toBe('markdown');
|
||||
expect(vert['B'].id).toBe('B');
|
||||
expect(vert['B'].text).toBe('The dog in the hog');
|
||||
expect(vert['B'].labelType).toBe('text');
|
||||
expect(vert['B'].labelType).toBe('string');
|
||||
expect(edges.length).toBe(2);
|
||||
expect(edges[0].start).toBe('A');
|
||||
expect(edges[0].end).toBe('B');
|
||||
@@ -35,7 +35,7 @@ A["\`The cat in **the** hat\`"]-- "\`The *bat* in the chat\`" -->B["The dog in t
|
||||
expect(edges[1].end).toBe('C');
|
||||
expect(edges[1].type).toBe('arrow_point');
|
||||
expect(edges[1].text).toBe('The rat in the mat');
|
||||
expect(edges[1].labelType).toBe('text');
|
||||
expect(edges[1].labelType).toBe('string');
|
||||
});
|
||||
it('mardown formatting in subgraphs', function () {
|
||||
const res = flow.parser.parse(`flowchart LR
|
||||
|
||||
@@ -6,6 +6,29 @@ setConfig({
|
||||
securityLevel: 'strict',
|
||||
});
|
||||
|
||||
const keywords = [
|
||||
'graph',
|
||||
'flowchart',
|
||||
'flowchart-elk',
|
||||
'style',
|
||||
'default',
|
||||
'linkStyle',
|
||||
'interpolate',
|
||||
'classDef',
|
||||
'class',
|
||||
'href',
|
||||
'call',
|
||||
'click',
|
||||
'_self',
|
||||
'_blank',
|
||||
'_parent',
|
||||
'_top',
|
||||
'end',
|
||||
'subgraph',
|
||||
];
|
||||
|
||||
const specialChars = ['#', ':', '0', '&', ',', '*', '.', '\\', 'v', '-', '/', '_'];
|
||||
|
||||
describe('[Singlenodes] when parsing', () => {
|
||||
beforeEach(function () {
|
||||
flow.parser.yy = flowDb;
|
||||
@@ -259,4 +282,90 @@ describe('[Singlenodes] when parsing', () => {
|
||||
expect(edges.length).toBe(0);
|
||||
expect(vert['i_d'].styles.length).toBe(0);
|
||||
});
|
||||
|
||||
it.each(keywords)('should handle keywords between dashes "-"', function (keyword) {
|
||||
const res = flow.parser.parse(`graph TD;a-${keyword}-node;`);
|
||||
const vert = flow.parser.yy.getVertices();
|
||||
expect(vert[`a-${keyword}-node`].text).toBe(`a-${keyword}-node`);
|
||||
});
|
||||
|
||||
it.each(keywords)('should handle keywords between periods "."', function (keyword) {
|
||||
const res = flow.parser.parse(`graph TD;a.${keyword}.node;`);
|
||||
const vert = flow.parser.yy.getVertices();
|
||||
expect(vert[`a.${keyword}.node`].text).toBe(`a.${keyword}.node`);
|
||||
});
|
||||
|
||||
it.each(keywords)('should handle keywords between underscores "_"', function (keyword) {
|
||||
const res = flow.parser.parse(`graph TD;a_${keyword}_node;`);
|
||||
const vert = flow.parser.yy.getVertices();
|
||||
expect(vert[`a_${keyword}_node`].text).toBe(`a_${keyword}_node`);
|
||||
});
|
||||
|
||||
it.each(keywords)('should handle nodes ending in %s', function (keyword) {
|
||||
const res = flow.parser.parse(`graph TD;node_${keyword};node.${keyword};node-${keyword};`);
|
||||
const vert = flow.parser.yy.getVertices();
|
||||
expect(vert[`node_${keyword}`].text).toBe(`node_${keyword}`);
|
||||
expect(vert[`node.${keyword}`].text).toBe(`node.${keyword}`);
|
||||
expect(vert[`node-${keyword}`].text).toBe(`node-${keyword}`);
|
||||
});
|
||||
|
||||
const errorKeywords = [
|
||||
'graph',
|
||||
'flowchart',
|
||||
'flowchart-elk',
|
||||
'style',
|
||||
'linkStyle',
|
||||
'interpolate',
|
||||
'classDef',
|
||||
'class',
|
||||
'_self',
|
||||
'_blank',
|
||||
'_parent',
|
||||
'_top',
|
||||
'end',
|
||||
'subgraph',
|
||||
];
|
||||
it.each(errorKeywords)('should throw error at nodes beginning with %s', function (keyword) {
|
||||
const str = `graph TD;${keyword}.node;${keyword}-node;${keyword}/node`;
|
||||
const vert = flow.parser.yy.getVertices();
|
||||
|
||||
expect(() => flow.parser.parse(str)).toThrowError();
|
||||
});
|
||||
|
||||
const workingKeywords = ['default', 'href', 'click', 'call'];
|
||||
|
||||
it.each(workingKeywords)('should parse node beginning with %s', function (keyword) {
|
||||
flow.parser.parse(`graph TD; ${keyword}.node;${keyword}-node;${keyword}/node;`);
|
||||
const vert = flow.parser.yy.getVertices();
|
||||
expect(vert[`${keyword}.node`].text).toBe(`${keyword}.node`);
|
||||
expect(vert[`${keyword}-node`].text).toBe(`${keyword}-node`);
|
||||
expect(vert[`${keyword}/node`].text).toBe(`${keyword}/node`);
|
||||
});
|
||||
|
||||
it.each(specialChars)(
|
||||
'should allow node ids of single special characters',
|
||||
function (specialChar) {
|
||||
flow.parser.parse(`graph TD; ${specialChar} --> A`);
|
||||
const vert = flow.parser.yy.getVertices();
|
||||
expect(vert[`${specialChar}`].text).toBe(`${specialChar}`);
|
||||
}
|
||||
);
|
||||
|
||||
it.each(specialChars)(
|
||||
'should allow node ids with special characters at start of id',
|
||||
function (specialChar) {
|
||||
flow.parser.parse(`graph TD; ${specialChar}node --> A`);
|
||||
const vert = flow.parser.yy.getVertices();
|
||||
expect(vert[`${specialChar}node`].text).toBe(`${specialChar}node`);
|
||||
}
|
||||
);
|
||||
|
||||
it.each(specialChars)(
|
||||
'should allow node ids with special characters at end of id',
|
||||
function (specialChar) {
|
||||
flow.parser.parse(`graph TD; node${specialChar} --> A`);
|
||||
const vert = flow.parser.yy.getVertices();
|
||||
expect(vert[`node${specialChar}`].text).toBe(`node${specialChar}`);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
@@ -26,15 +26,6 @@ describe('[Style] when parsing', () => {
|
||||
expect(vert['Q'].styles[0]).toBe('background:#fff');
|
||||
});
|
||||
|
||||
// log.debug(flow.parser.parse('graph TD;style Q background:#fff;'));
|
||||
it('should handle styles for edges', function () {
|
||||
const res = flow.parser.parse('graph TD;a-->b;\nstyle #0 stroke: #f66;');
|
||||
|
||||
const edges = flow.parser.yy.getEdges();
|
||||
|
||||
expect(edges.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should handle multiple styles for a vortex', function () {
|
||||
const res = flow.parser.parse('graph TD;style R background:#fff,border:1px solid red;');
|
||||
|
||||
|
||||
@@ -305,6 +305,95 @@ describe('[Text] when parsing', () => {
|
||||
expect(vert['C'].type).toBe('round');
|
||||
expect(vert['C'].text).toBe('Chimpansen hoppar');
|
||||
});
|
||||
|
||||
const keywords = [
|
||||
'graph',
|
||||
'flowchart',
|
||||
'flowchart-elk',
|
||||
'style',
|
||||
'default',
|
||||
'linkStyle',
|
||||
'interpolate',
|
||||
'classDef',
|
||||
'class',
|
||||
'href',
|
||||
'call',
|
||||
'click',
|
||||
'_self',
|
||||
'_blank',
|
||||
'_parent',
|
||||
'_top',
|
||||
'end',
|
||||
'subgraph',
|
||||
'kitty',
|
||||
];
|
||||
|
||||
const shapes = [
|
||||
{ start: '[', end: ']', name: 'square' },
|
||||
{ start: '(', end: ')', name: 'round' },
|
||||
{ start: '{', end: '}', name: 'diamond' },
|
||||
{ start: '(-', end: '-)', name: 'ellipse' },
|
||||
{ start: '([', end: '])', name: 'stadium' },
|
||||
{ start: '>', end: ']', name: 'odd' },
|
||||
{ start: '[(', end: ')]', name: 'cylinder' },
|
||||
{ start: '(((', end: ')))', name: 'doublecircle' },
|
||||
{ start: '[/', end: '\\]', name: 'trapezoid' },
|
||||
{ start: '[\\', end: '/]', name: 'inv_trapezoid' },
|
||||
{ start: '[/', end: '/]', name: 'lean_right' },
|
||||
{ start: '[\\', end: '\\]', name: 'lean_left' },
|
||||
{ start: '[[', end: ']]', name: 'subroutine' },
|
||||
{ start: '{{', end: '}}', name: 'hexagon' },
|
||||
];
|
||||
|
||||
shapes.forEach((shape) => {
|
||||
it.each(keywords)(`should handle %s keyword in ${shape.name} vertex`, function (keyword) {
|
||||
const rest = flow.parser.parse(
|
||||
`graph TD;A_${keyword}_node-->B${shape.start}This node has a ${keyword} as text${shape.end};`
|
||||
);
|
||||
|
||||
const vert = flow.parser.yy.getVertices();
|
||||
const edges = flow.parser.yy.getEdges();
|
||||
expect(vert['B'].type).toBe(`${shape.name}`);
|
||||
expect(vert['B'].text).toBe(`This node has a ${keyword} as text`);
|
||||
});
|
||||
});
|
||||
|
||||
it.each(keywords)('should handle %s keyword in rect vertex', function (keyword) {
|
||||
const rest = flow.parser.parse(
|
||||
`graph TD;A_${keyword}_node-->B[|borders:lt|This node has a ${keyword} as text];`
|
||||
);
|
||||
|
||||
const vert = flow.parser.yy.getVertices();
|
||||
const edges = flow.parser.yy.getEdges();
|
||||
expect(vert['B'].type).toBe('rect');
|
||||
expect(vert['B'].text).toBe(`This node has a ${keyword} as text`);
|
||||
});
|
||||
|
||||
it('should handle edge case for odd vertex with node id ending with minus', function () {
|
||||
const res = flow.parser.parse('graph TD;A_node-->odd->Vertex Text];');
|
||||
const vert = flow.parser.yy.getVertices();
|
||||
|
||||
expect(vert['odd-'].type).toBe('odd');
|
||||
expect(vert['odd-'].text).toBe('Vertex Text');
|
||||
});
|
||||
it('should allow forward slashes in lean_right vertices', function () {
|
||||
const rest = flow.parser.parse(`graph TD;A_node-->B[/This node has a / as text/];`);
|
||||
|
||||
const vert = flow.parser.yy.getVertices();
|
||||
const edges = flow.parser.yy.getEdges();
|
||||
expect(vert['B'].type).toBe('lean_right');
|
||||
expect(vert['B'].text).toBe(`This node has a / as text`);
|
||||
});
|
||||
|
||||
it('should allow back slashes in lean_left vertices', function () {
|
||||
const rest = flow.parser.parse(`graph TD;A_node-->B[\\This node has a \\ as text\\];`);
|
||||
|
||||
const vert = flow.parser.yy.getVertices();
|
||||
const edges = flow.parser.yy.getEdges();
|
||||
expect(vert['B'].type).toBe('lean_left');
|
||||
expect(vert['B'].text).toBe(`This node has a \\ as text`);
|
||||
});
|
||||
|
||||
it('should handle åäö and minus', function () {
|
||||
const res = flow.parser.parse('graph TD;A-->C{Chimpansen hoppar åäö-ÅÄÖ};');
|
||||
|
||||
@@ -484,4 +573,33 @@ describe('[Text] when parsing', () => {
|
||||
expect(vert['A'].text).toBe(',.?!+-*');
|
||||
expect(edges[0].text).toBe(',.?!+-*');
|
||||
});
|
||||
|
||||
it('should throw error at nested set of brackets', function () {
|
||||
const str = 'graph TD; A[This is a () in text];';
|
||||
expect(() => flow.parser.parse(str)).toThrowError("got 'PS'");
|
||||
});
|
||||
|
||||
it('should throw error for strings and text at the same time', function () {
|
||||
const str = 'graph TD;A(this node has "string" and text)-->|this link has "string" and text|C;';
|
||||
|
||||
expect(() => flow.parser.parse(str)).toThrowError("got 'STR'");
|
||||
});
|
||||
|
||||
it('should throw error for escaping quotes in text state', function () {
|
||||
//prettier-ignore
|
||||
const str = 'graph TD; A[This is a \"()\" in text];'; //eslint-disable-line no-useless-escape
|
||||
|
||||
expect(() => flow.parser.parse(str)).toThrowError("got 'STR'");
|
||||
});
|
||||
|
||||
it('should throw error for nested quoatation marks', function () {
|
||||
const str = 'graph TD; A["This is a "()" in text"];';
|
||||
|
||||
expect(() => flow.parser.parse(str)).toThrowError("Expecting 'SQE'");
|
||||
});
|
||||
|
||||
it('should throw error', function () {
|
||||
const str = `graph TD; node[hello ) world] --> works`;
|
||||
expect(() => flow.parser.parse(str)).toThrowError("got 'PE'");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,51 +13,26 @@
|
||||
%x acc_descr_multiline
|
||||
%x dir
|
||||
%x vertex
|
||||
%x text
|
||||
%x ellipseText
|
||||
%x trapText
|
||||
%x edgeText
|
||||
%x thickEdgeText
|
||||
%x dottedEdgeText
|
||||
%x click
|
||||
%x href
|
||||
%x callbackname
|
||||
%x callbackargs
|
||||
%x open_directive
|
||||
%x type_directive
|
||||
%x arg_directive
|
||||
%x close_directive
|
||||
|
||||
%%
|
||||
\%\%\{ { this.begin('open_directive'); return 'open_directive'; }
|
||||
<open_directive>((?:(?!\}\%\%)[^:.])*) { this.begin('type_directive'); return 'type_directive'; }
|
||||
<type_directive>":" { this.popState(); this.begin('arg_directive'); return ':'; }
|
||||
<type_directive,arg_directive>\}\%\% { this.popState(); this.popState(); return 'close_directive'; }
|
||||
<arg_directive>((?:(?!\}\%\%).|\n)*) return 'arg_directive';
|
||||
accTitle\s*":"\s* { this.begin("acc_title");return 'acc_title'; }
|
||||
<acc_title>(?!\n|;|#)*[^\n]* { this.popState(); return "acc_title_value"; }
|
||||
accDescr\s*":"\s* { this.begin("acc_descr");return 'acc_descr'; }
|
||||
<acc_descr>(?!\n|;|#)*[^\n]* { this.popState(); return "acc_descr_value"; }
|
||||
accDescr\s*"{"\s* { this.begin("acc_descr_multiline");}
|
||||
accTitle\s*":"\s* { this.begin("acc_title");return 'acc_title'; }
|
||||
<acc_title>(?!\n|;|#)*[^\n]* { this.popState(); return "acc_title_value"; }
|
||||
accDescr\s*":"\s* { this.begin("acc_descr");return 'acc_descr'; }
|
||||
<acc_descr>(?!\n|;|#)*[^\n]* { this.popState(); return "acc_descr_value"; }
|
||||
accDescr\s*"{"\s* { this.begin("acc_descr_multiline");}
|
||||
<acc_descr_multiline>[\}] { this.popState(); }
|
||||
<acc_descr_multiline>[^\}]* return "acc_descr_multiline_value";
|
||||
// <acc_descr_multiline>.*[^\n]* { return "acc_descr_line"}
|
||||
["][`] { this.begin("md_string");}
|
||||
<md_string>[^`"]+ { return "MD_STR";}
|
||||
<md_string>[`]["] { this.popState();}
|
||||
["] this.begin("string");
|
||||
<string>["] this.popState();
|
||||
<string>[^"]* return "STR";
|
||||
"style" return 'STYLE';
|
||||
"default" return 'DEFAULT';
|
||||
"linkStyle" return 'LINKSTYLE';
|
||||
"interpolate" return 'INTERPOLATE';
|
||||
"classDef" return 'CLASSDEF';
|
||||
"class" return 'CLASS';
|
||||
|
||||
/*
|
||||
---interactivity command---
|
||||
'href' adds a link to the specified node. 'href' can only be specified when the
|
||||
line was introduced with 'click'.
|
||||
'href "<link>"' attaches the specified link to the node that was specified by 'click'.
|
||||
*/
|
||||
"href"[\s]+["] this.begin("href");
|
||||
<href>["] this.popState();
|
||||
<href>[^"]* return 'HREF';
|
||||
// <acc_descr_multiline>.*[^\n]* { return "acc_descr_line"}
|
||||
|
||||
/*
|
||||
---interactivity command---
|
||||
@@ -74,88 +49,128 @@ Function arguments are optional: 'call <callbackname>()' simply executes 'callba
|
||||
<callbackargs>\) this.popState();
|
||||
<callbackargs>[^)]* return 'CALLBACKARGS';
|
||||
|
||||
<md_string>[^`"]+ { return "MD_STR";}
|
||||
<md_string>[`]["] { this.popState();}
|
||||
<*>["][`] { this.begin("md_string");}
|
||||
<string>[^"]+ return "STR";
|
||||
<string>["] this.popState();
|
||||
<*>["] this.pushState("string");
|
||||
"style" return 'STYLE';
|
||||
"default" return 'DEFAULT';
|
||||
"linkStyle" return 'LINKSTYLE';
|
||||
"interpolate" return 'INTERPOLATE';
|
||||
"classDef" return 'CLASSDEF';
|
||||
"class" return 'CLASS';
|
||||
|
||||
/*
|
||||
---interactivity command---
|
||||
'href' adds a link to the specified node. 'href' can only be specified when the
|
||||
line was introduced with 'click'.
|
||||
'href "<link>"' attaches the specified link to the node that was specified by 'click'.
|
||||
*/
|
||||
"href"[\s] return 'HREF';
|
||||
|
||||
|
||||
/*
|
||||
'click' is the keyword to introduce a line that contains interactivity commands.
|
||||
'click' must be followed by an existing node-id. All commands are attached to
|
||||
that id.
|
||||
'click <id>' can be followed by href or call commands in any desired order
|
||||
*/
|
||||
"click"[\s]+ this.begin("click");
|
||||
<click>[\s\n] this.popState();
|
||||
<click>[^\s\n]* return 'CLICK';
|
||||
"click"[\s]+ this.begin("click");
|
||||
<click>[\s\n] this.popState();
|
||||
<click>[^\s\n]* return 'CLICK';
|
||||
|
||||
"flowchart-elk" {if(yy.lex.firstGraph()){this.begin("dir");} return 'GRAPH';}
|
||||
"graph" {if(yy.lex.firstGraph()){this.begin("dir");} return 'GRAPH';}
|
||||
"flowchart" {if(yy.lex.firstGraph()){this.begin("dir");} return 'GRAPH';}
|
||||
"subgraph" return 'subgraph';
|
||||
"end"\b\s* return 'end';
|
||||
"flowchart-elk" {if(yy.lex.firstGraph()){this.begin("dir");} return 'GRAPH';}
|
||||
"graph" {if(yy.lex.firstGraph()){this.begin("dir");} return 'GRAPH';}
|
||||
"flowchart" {if(yy.lex.firstGraph()){this.begin("dir");} return 'GRAPH';}
|
||||
"subgraph" return 'subgraph';
|
||||
"end"\b\s* return 'end';
|
||||
|
||||
"_self" return 'LINK_TARGET';
|
||||
"_blank" return 'LINK_TARGET';
|
||||
"_parent" return 'LINK_TARGET';
|
||||
"_top" return 'LINK_TARGET';
|
||||
"_self" return 'LINK_TARGET';
|
||||
"_blank" return 'LINK_TARGET';
|
||||
"_parent" return 'LINK_TARGET';
|
||||
"_top" return 'LINK_TARGET';
|
||||
|
||||
<dir>(\r?\n)*\s*\n { this.popState(); return 'NODIR'; }
|
||||
<dir>\s*"LR" { this.popState(); return 'DIR'; }
|
||||
<dir>\s*"RL" { this.popState(); return 'DIR'; }
|
||||
<dir>\s*"TB" { this.popState(); return 'DIR'; }
|
||||
<dir>\s*"BT" { this.popState(); return 'DIR'; }
|
||||
<dir>\s*"TD" { this.popState(); return 'DIR'; }
|
||||
<dir>\s*"BR" { this.popState(); return 'DIR'; }
|
||||
<dir>\s*"<" { this.popState(); return 'DIR'; }
|
||||
<dir>\s*">" { this.popState(); return 'DIR'; }
|
||||
<dir>\s*"^" { this.popState(); return 'DIR'; }
|
||||
<dir>\s*"v" { this.popState(); return 'DIR'; }
|
||||
<dir>(\r?\n)*\s*\n { this.popState(); return 'NODIR'; }
|
||||
<dir>\s*"LR" { this.popState(); return 'DIR'; }
|
||||
<dir>\s*"RL" { this.popState(); return 'DIR'; }
|
||||
<dir>\s*"TB" { this.popState(); return 'DIR'; }
|
||||
<dir>\s*"BT" { this.popState(); return 'DIR'; }
|
||||
<dir>\s*"TD" { this.popState(); return 'DIR'; }
|
||||
<dir>\s*"BR" { this.popState(); return 'DIR'; }
|
||||
<dir>\s*"<" { this.popState(); return 'DIR'; }
|
||||
<dir>\s*">" { this.popState(); return 'DIR'; }
|
||||
<dir>\s*"^" { this.popState(); return 'DIR'; }
|
||||
<dir>\s*"v" { this.popState(); return 'DIR'; }
|
||||
|
||||
.*direction\s+TB[^\n]* return 'direction_tb';
|
||||
.*direction\s+BT[^\n]* return 'direction_bt';
|
||||
.*direction\s+RL[^\n]* return 'direction_rl';
|
||||
.*direction\s+LR[^\n]* return 'direction_lr';
|
||||
|
||||
[0-9]+ return 'NUM';
|
||||
\# return 'BRKT';
|
||||
":::" return 'STYLE_SEPARATOR';
|
||||
":" return 'COLON';
|
||||
"&" return 'AMP';
|
||||
";" return 'SEMI';
|
||||
"," return 'COMMA';
|
||||
"*" return 'MULT';
|
||||
|
||||
<INITIAL,edgeText>\s*[xo<]?\-\-+[-xo>]\s* { this.popState(); return 'LINK'; }
|
||||
<INITIAL>\s*[xo<]?\-\-\s* { this.pushState("edgeText"); return 'START_LINK'; }
|
||||
<edgeText>[^-]|\-(?!\-)+ return 'EDGE_TEXT';
|
||||
|
||||
<INITIAL,thickEdgeText>\s*[xo<]?\=\=+[=xo>]\s* { this.popState(); return 'LINK'; }
|
||||
<INITIAL>\s*[xo<]?\=\=\s* { this.pushState("thickEdgeText"); return 'START_LINK'; }
|
||||
<thickEdgeText>[^=]|\=(?!=) return 'EDGE_TEXT';
|
||||
|
||||
<INITIAL,dottedEdgeText>\s*[xo<]?\-?\.+\-[xo>]?\s* { this.popState(); return 'LINK'; }
|
||||
<INITIAL>\s*[xo<]?\-\.\s* { this.pushState("dottedEdgeText"); return 'START_LINK'; }
|
||||
<dottedEdgeText>[^\.]|\.(?!-) return 'EDGE_TEXT';
|
||||
|
||||
|
||||
<*>\s*\~\~[\~]+\s* return 'LINK';
|
||||
|
||||
<ellipseText>[-/\)][\)] { this.popState(); return '-)'; }
|
||||
<ellipseText>[^\(\)\[\]\{\}]|-/!\)+ return "TEXT"
|
||||
<*>"(-" { this.pushState("ellipseText"); return '(-'; }
|
||||
|
||||
<text>"])" { this.popState(); return 'STADIUMEND'; }
|
||||
<*>"([" { this.pushState("text"); return 'STADIUMSTART'; }
|
||||
|
||||
<text>"]]" { this.popState(); return 'SUBROUTINEEND'; }
|
||||
<*>"[[" { this.pushState("text"); return 'SUBROUTINESTART'; }
|
||||
|
||||
"[|" { return 'VERTEX_WITH_PROPS_START'; }
|
||||
|
||||
\> { this.pushState("text"); return 'TAGEND'; }
|
||||
|
||||
<text>")]" { this.popState(); return 'CYLINDEREND'; }
|
||||
<*>"[(" { this.pushState("text") ;return 'CYLINDERSTART'; }
|
||||
|
||||
<text>")))" { this.popState(); return 'DOUBLECIRCLEEND'; }
|
||||
<*>"(((" { this.pushState("text"); return 'DOUBLECIRCLESTART'; }
|
||||
|
||||
<trapText>[\\(?=\])][\]] { this.popState(); return 'TRAPEND'; }
|
||||
<trapText>\/(?=\])\] { this.popState(); return 'INVTRAPEND'; }
|
||||
<trapText>\/(?!\])|\\(?!\])|[^\\\[\]\(\)\{\}\/]+ return 'TEXT';
|
||||
<*>"[/" { this.pushState("trapText"); return 'TRAPSTART'; }
|
||||
|
||||
<*>"[\\" { this.pushState("trapText"); return 'INVTRAPSTART'; }
|
||||
|
||||
.*direction\s+TB[^\n]* return 'direction_tb';
|
||||
.*direction\s+BT[^\n]* return 'direction_bt';
|
||||
.*direction\s+RL[^\n]* return 'direction_rl';
|
||||
.*direction\s+LR[^\n]* return 'direction_lr';
|
||||
|
||||
[0-9]+ { return 'NUM';}
|
||||
\# return 'BRKT';
|
||||
":::" return 'STYLE_SEPARATOR';
|
||||
":" return 'COLON';
|
||||
"&" return 'AMP';
|
||||
";" return 'SEMI';
|
||||
"," return 'COMMA';
|
||||
"*" return 'MULT';
|
||||
\s*[xo<]?\-\-+[-xo>]\s* return 'LINK';
|
||||
\s*[xo<]?\=\=+[=xo>]\s* return 'LINK';
|
||||
\s*[xo<]?\-?\.+\-[xo>]?\s* return 'LINK';
|
||||
\s*\~\~[\~]+\s* return 'LINK';
|
||||
\s*[xo<]?\-\-\s* return 'START_LINK';
|
||||
\s*[xo<]?\=\=\s* return 'START_LINK';
|
||||
\s*[xo<]?\-\.\s* return 'START_LINK';
|
||||
"(-" return '(-';
|
||||
"-)" return '-)';
|
||||
"([" return 'STADIUMSTART';
|
||||
"])" return 'STADIUMEND';
|
||||
"[[" return 'SUBROUTINESTART';
|
||||
"]]" return 'SUBROUTINEEND';
|
||||
"[|" return 'VERTEX_WITH_PROPS_START';
|
||||
"[(" return 'CYLINDERSTART';
|
||||
")]" return 'CYLINDEREND';
|
||||
"(((" return 'DOUBLECIRCLESTART';
|
||||
")))" return 'DOUBLECIRCLEEND';
|
||||
\- return 'MINUS';
|
||||
"." return 'DOT';
|
||||
[\_] return 'UNDERSCORE';
|
||||
\+ return 'PLUS';
|
||||
\% return 'PCT';
|
||||
"=" return 'EQUALS';
|
||||
\= return 'EQUALS';
|
||||
"<" return 'TAGSTART';
|
||||
">" return 'TAGEND';
|
||||
"^" return 'UP';
|
||||
"\|" return 'SEP';
|
||||
"v" return 'DOWN';
|
||||
[A-Za-z]+ return 'ALPHA';
|
||||
"\\]" return 'TRAPEND';
|
||||
"[/" return 'TRAPSTART';
|
||||
"/]" return 'INVTRAPEND';
|
||||
"[\\" return 'INVTRAPSTART';
|
||||
[!"#$%&'*+,-.`?\\_/] return 'PUNCTUATION';
|
||||
"*" return 'MULT';
|
||||
"#" return 'BRKT';
|
||||
"&" return 'AMP';
|
||||
([A-Za-z0-9!"\#$%&'*+\.`?\\_\/]|\-(?=[^\>\-\.])|=(?!=))+ return 'NODE_STRING';
|
||||
"-" return 'MINUS'
|
||||
[\u00AA\u00B5\u00BA\u00C0-\u00D6\u00D8-\u00F6]|
|
||||
[\u00F8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377]|
|
||||
[\u037A-\u037D\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5]|
|
||||
@@ -218,13 +233,20 @@ that id.
|
||||
[\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF]|
|
||||
[\uFFD2-\uFFD7\uFFDA-\uFFDC]
|
||||
return 'UNICODE_TEXT';
|
||||
"|" return 'PIPE';
|
||||
"(" return 'PS';
|
||||
")" return 'PE';
|
||||
"[" return 'SQS';
|
||||
"]" return 'SQE';
|
||||
"{" return 'DIAMOND_START'
|
||||
"}" return 'DIAMOND_STOP'
|
||||
|
||||
<text>"|" { this.popState(); return 'PIPE'; }
|
||||
<*>"|" { this.pushState("text"); return 'PIPE'; }
|
||||
|
||||
<text>")" { this.popState(); return 'PE'; }
|
||||
<*>"(" { this.pushState("text"); return 'PS'; }
|
||||
|
||||
<text>"]" { this.popState(); return 'SQE'; }
|
||||
<*>"[" { this.pushState("text"); return 'SQS'; }
|
||||
|
||||
<text>(\}) { this.popState(); return 'DIAMOND_STOP' }
|
||||
<*>"{" { this.pushState("text"); return 'DIAMOND_START' }
|
||||
<text>[^\[\]\(\)\{\}\|\"]+ return "TEXT";
|
||||
|
||||
"\"" return 'QUOTE';
|
||||
(\r?\n)+ return 'NEWLINE';
|
||||
\s return 'SPACE';
|
||||
@@ -241,49 +263,24 @@ that id.
|
||||
%% /* language grammar */
|
||||
|
||||
start
|
||||
: mermaidDoc
|
||||
| directive start
|
||||
;
|
||||
|
||||
directive
|
||||
: openDirective typeDirective closeDirective separator
|
||||
| openDirective typeDirective ':' argDirective closeDirective separator
|
||||
;
|
||||
|
||||
openDirective
|
||||
: open_directive { yy.parseDirective('%%{', 'open_directive'); }
|
||||
;
|
||||
|
||||
typeDirective
|
||||
: type_directive { yy.parseDirective($1, 'type_directive'); }
|
||||
;
|
||||
|
||||
argDirective
|
||||
: arg_directive { $1 = $1.trim().replace(/'/g, '"'); yy.parseDirective($1, 'arg_directive'); }
|
||||
;
|
||||
|
||||
closeDirective
|
||||
: close_directive { yy.parseDirective('}%%', 'close_directive', 'flowchart'); }
|
||||
;
|
||||
|
||||
mermaidDoc
|
||||
: graphConfig document
|
||||
;
|
||||
|
||||
|
||||
document
|
||||
: /* empty */
|
||||
{ $$ = [];}
|
||||
| document line
|
||||
{
|
||||
if(!Array.isArray($2) || $2.length > 0){
|
||||
$1.push($2);
|
||||
if(!Array.isArray($line) || $line.length > 0){
|
||||
$document.push($line);
|
||||
}
|
||||
$$=$1;}
|
||||
$$=$document;}
|
||||
;
|
||||
|
||||
line
|
||||
: statement
|
||||
{$$=$1;}
|
||||
{$$=$statement;}
|
||||
| SEMI
|
||||
| NEWLINE
|
||||
| SPACE
|
||||
@@ -296,15 +293,15 @@ graphConfig
|
||||
| GRAPH NODIR
|
||||
{ yy.setDirection('TB');$$ = 'TB';}
|
||||
| GRAPH DIR FirstStmtSeperator
|
||||
{ yy.setDirection($2);$$ = $2;}
|
||||
{ yy.setDirection($DIR);$$ = $DIR;}
|
||||
// | GRAPH SPACE TAGEND FirstStmtSeperator
|
||||
// { yy.setDirection("LR");$$ = $3;}
|
||||
// { yy.setDirection("LR");$$ = $TAGEND;}
|
||||
// | GRAPH SPACE TAGSTART FirstStmtSeperator
|
||||
// { yy.setDirection("RL");$$ = $3;}
|
||||
// { yy.setDirection("RL");$$ = $TAGSTART;}
|
||||
// | GRAPH SPACE UP FirstStmtSeperator
|
||||
// { yy.setDirection("BT");$$ = $3;}
|
||||
// { yy.setDirection("BT");$$ = $UP;}
|
||||
// | GRAPH SPACE DOWN FirstStmtSeperator
|
||||
// { yy.setDirection("TB");$$ = $3;}
|
||||
// { yy.setDirection("TB");$$ = $DOWN;}
|
||||
;
|
||||
|
||||
ending: endToken ending
|
||||
@@ -332,7 +329,7 @@ spaceList
|
||||
|
||||
statement
|
||||
: verticeStatement separator
|
||||
{ /* console.warn('finat vs', $1.nodes); */ $$=$1.nodes}
|
||||
{ /* console.warn('finat vs', $verticeStatement.nodes); */ $$=$verticeStatement.nodes}
|
||||
| styleStatement separator
|
||||
{$$=[];}
|
||||
| linkStyleStatement separator
|
||||
@@ -343,110 +340,121 @@ statement
|
||||
{$$=[];}
|
||||
| clickStatement separator
|
||||
{$$=[];}
|
||||
| subgraph SPACE text SQS text SQE separator document end
|
||||
{$$=yy.addSubGraph($3,$8,$5);}
|
||||
| subgraph SPACE text separator document end
|
||||
{$$=yy.addSubGraph($3,$5,$3);}
|
||||
// | subgraph SPACE text separator document end
|
||||
// {$$=yy.addSubGraph($3,$5,$3);}
|
||||
| subgraph SPACE textNoTags SQS text SQE separator document end
|
||||
{$$=yy.addSubGraph($textNoTags,$document,$text);}
|
||||
| subgraph SPACE textNoTags separator document end
|
||||
{$$=yy.addSubGraph($textNoTags,$document,$textNoTags);}
|
||||
// | subgraph SPACE textNoTags separator document end
|
||||
// {$$=yy.addSubGraph($textNoTags,$document,$textNoTags);}
|
||||
| subgraph separator document end
|
||||
{$$=yy.addSubGraph(undefined,$3,undefined);}
|
||||
{$$=yy.addSubGraph(undefined,$document,undefined);}
|
||||
| direction
|
||||
| acc_title acc_title_value { $$=$2.trim();yy.setAccTitle($$); }
|
||||
| acc_descr acc_descr_value { $$=$2.trim();yy.setAccDescription($$); }
|
||||
| acc_descr_multiline_value { $$=$1.trim();yy.setAccDescription($$); }
|
||||
| acc_title acc_title_value { $$=$acc_title_value.trim();yy.setAccTitle($$); }
|
||||
| acc_descr acc_descr_value { $$=$acc_descr_value.trim();yy.setAccDescription($$); }
|
||||
| acc_descr_multiline_value { $$=$acc_descr_multiline_value.trim();yy.setAccDescription($$); }
|
||||
;
|
||||
|
||||
separator: NEWLINE | SEMI | EOF ;
|
||||
|
||||
|
||||
|
||||
verticeStatement: verticeStatement link node
|
||||
{ /* console.warn('vs',$1.stmt,$3); */ yy.addLink($1.stmt,$3,$2); $$ = { stmt: $3, nodes: $3.concat($1.nodes) } }
|
||||
{ /* console.warn('vs',$verticeStatement.stmt,$node); */ yy.addLink($verticeStatement.stmt,$node,$link); $$ = { stmt: $node, nodes: $node.concat($verticeStatement.nodes) } }
|
||||
| verticeStatement link node spaceList
|
||||
{ /* console.warn('vs',$1.stmt,$3); */ yy.addLink($1.stmt,$3,$2); $$ = { stmt: $3, nodes: $3.concat($1.nodes) } }
|
||||
|node spaceList {/*console.warn('noda', $1);*/ $$ = {stmt: $1, nodes:$1 }}
|
||||
|node { /*console.warn('noda', $1);*/ $$ = {stmt: $1, nodes:$1 }}
|
||||
{ /* console.warn('vs',$verticeStatement.stmt,$node); */ yy.addLink($verticeStatement.stmt,$node,$link); $$ = { stmt: $node, nodes: $node.concat($verticeStatement.nodes) } }
|
||||
|node spaceList {/*console.warn('noda', $node);*/ $$ = {stmt: $node, nodes:$node }}
|
||||
|node { /*console.warn('noda', $node);*/ $$ = {stmt: $node, nodes:$node }}
|
||||
;
|
||||
|
||||
node: styledVertex
|
||||
{ /* console.warn('nod', $1); */ $$ = [$1];}
|
||||
{ /* console.warn('nod', $styledVertex); */ $$ = [$styledVertex];}
|
||||
| node spaceList AMP spaceList styledVertex
|
||||
{ $$ = $1.concat($5); /* console.warn('pip', $1[0], $5, $$); */ }
|
||||
{ $$ = $node.concat($styledVertex); /* console.warn('pip', $node[0], $styledVertex, $$); */ }
|
||||
;
|
||||
|
||||
styledVertex: vertex
|
||||
{ /* console.warn('nod', $1); */ $$ = $1;}
|
||||
{ /* console.warn('nod', $vertex); */ $$ = $vertex;}
|
||||
| vertex STYLE_SEPARATOR idString
|
||||
{$$ = $1;yy.setClass($1,$3)}
|
||||
{$$ = $vertex;yy.setClass($vertex,$idString)}
|
||||
;
|
||||
|
||||
vertex: idString SQS text SQE
|
||||
{$$ = $1;yy.addVertex($1,$3,'square');}
|
||||
{$$ = $idString;yy.addVertex($idString,$text,'square');}
|
||||
| idString DOUBLECIRCLESTART text DOUBLECIRCLEEND
|
||||
{$$ = $1;yy.addVertex($1,$3,'doublecircle');}
|
||||
{$$ = $idString;yy.addVertex($idString,$text,'doublecircle');}
|
||||
| idString PS PS text PE PE
|
||||
{$$ = $1;yy.addVertex($1,$4,'circle');}
|
||||
{$$ = $idString;yy.addVertex($idString,$text,'circle');}
|
||||
| idString '(-' text '-)'
|
||||
{$$ = $1;yy.addVertex($1,$3,'ellipse');}
|
||||
{$$ = $idString;yy.addVertex($idString,$text,'ellipse');}
|
||||
| idString STADIUMSTART text STADIUMEND
|
||||
{$$ = $1;yy.addVertex($1,$3,'stadium');}
|
||||
{$$ = $idString;yy.addVertex($idString,$text,'stadium');}
|
||||
| idString SUBROUTINESTART text SUBROUTINEEND
|
||||
{$$ = $1;yy.addVertex($1,$3,'subroutine');}
|
||||
| idString VERTEX_WITH_PROPS_START ALPHA COLON ALPHA PIPE text SQE
|
||||
{$$ = $1;yy.addVertex($1,$7,'rect',undefined,undefined,undefined, Object.fromEntries([[$3, $5]]));}
|
||||
{$$ = $idString;yy.addVertex($idString,$text,'subroutine');}
|
||||
| idString VERTEX_WITH_PROPS_START NODE_STRING\[field] COLON NODE_STRING\[value] PIPE text SQE
|
||||
{$$ = $idString;yy.addVertex($idString,$text,'rect',undefined,undefined,undefined, Object.fromEntries([[$field, $value]]));}
|
||||
| idString CYLINDERSTART text CYLINDEREND
|
||||
{$$ = $1;yy.addVertex($1,$3,'cylinder');}
|
||||
{$$ = $idString;yy.addVertex($idString,$text,'cylinder');}
|
||||
| idString PS text PE
|
||||
{$$ = $1;yy.addVertex($1,$3,'round');}
|
||||
{$$ = $idString;yy.addVertex($idString,$text,'round');}
|
||||
| idString DIAMOND_START text DIAMOND_STOP
|
||||
{$$ = $1;yy.addVertex($1,$3,'diamond');}
|
||||
{$$ = $idString;yy.addVertex($idString,$text,'diamond');}
|
||||
| idString DIAMOND_START DIAMOND_START text DIAMOND_STOP DIAMOND_STOP
|
||||
{$$ = $1;yy.addVertex($1,$4,'hexagon');}
|
||||
{$$ = $idString;yy.addVertex($idString,$text,'hexagon');}
|
||||
| idString TAGEND text SQE
|
||||
{$$ = $1;yy.addVertex($1,$3,'odd');}
|
||||
{$$ = $idString;yy.addVertex($idString,$text,'odd');}
|
||||
| idString TRAPSTART text TRAPEND
|
||||
{$$ = $1;yy.addVertex($1,$3,'trapezoid');}
|
||||
{$$ = $idString;yy.addVertex($idString,$text,'trapezoid');}
|
||||
| idString INVTRAPSTART text INVTRAPEND
|
||||
{$$ = $1;yy.addVertex($1,$3,'inv_trapezoid');}
|
||||
{$$ = $idString;yy.addVertex($idString,$text,'inv_trapezoid');}
|
||||
| idString TRAPSTART text INVTRAPEND
|
||||
{$$ = $1;yy.addVertex($1,$3,'lean_right');}
|
||||
{$$ = $idString;yy.addVertex($idString,$text,'lean_right');}
|
||||
| idString INVTRAPSTART text TRAPEND
|
||||
{$$ = $1;yy.addVertex($1,$3,'lean_left');}
|
||||
{$$ = $idString;yy.addVertex($idString,$text,'lean_left');}
|
||||
| idString
|
||||
{ /*console.warn('h: ', $1);*/$$ = $1;yy.addVertex($1);}
|
||||
{ /*console.warn('h: ', $idString);*/$$ = $idString;yy.addVertex($idString);}
|
||||
;
|
||||
|
||||
|
||||
|
||||
link: linkStatement arrowText
|
||||
{$1.text = $2;$$ = $1;}
|
||||
{$linkStatement.text = $arrowText;$$ = $linkStatement;}
|
||||
| linkStatement TESTSTR SPACE
|
||||
{$1.text = $2;$$ = $1;}
|
||||
{$linkStatement.text = $TESTSTR;$$ = $linkStatement;}
|
||||
| linkStatement arrowText SPACE
|
||||
{$1.text = $2;$$ = $1;}
|
||||
{$linkStatement.text = $arrowText;$$ = $linkStatement;}
|
||||
| linkStatement
|
||||
{$$ = $1;}
|
||||
| START_LINK text LINK
|
||||
{var inf = yy.destructLink($3, $1); $$ = {"type":inf.type,"stroke":inf.stroke,"length":inf.length,"text":$2};}
|
||||
{$$ = $linkStatement;}
|
||||
| START_LINK edgeText LINK
|
||||
{var inf = yy.destructLink($LINK, $START_LINK); $$ = {"type":inf.type,"stroke":inf.stroke,"length":inf.length,"text":$edgeText};}
|
||||
;
|
||||
|
||||
edgeText: edgeTextToken
|
||||
{$$={text:$edgeTextToken, type:'text'};}
|
||||
| edgeText edgeTextToken
|
||||
{$$={text:$edgeText.text+''+$edgeTextToken, type:$edgeText.type};}
|
||||
|STR
|
||||
{$$={text: $STR, type: 'string'};}
|
||||
| MD_STR
|
||||
{$$={text:$MD_STR, type:'markdown'};}
|
||||
;
|
||||
|
||||
|
||||
linkStatement: LINK
|
||||
{var inf = yy.destructLink($1);$$ = {"type":inf.type,"stroke":inf.stroke,"length":inf.length};}
|
||||
{var inf = yy.destructLink($LINK);$$ = {"type":inf.type,"stroke":inf.stroke,"length":inf.length};}
|
||||
;
|
||||
|
||||
arrowText:
|
||||
PIPE text PIPE
|
||||
{$$ = $2;}
|
||||
{$$ = $text;}
|
||||
;
|
||||
|
||||
text: textToken
|
||||
{ $$={text:$1, type: 'text'};}
|
||||
{ $$={text:$textToken, type: 'text'};}
|
||||
| text textToken
|
||||
{ $$={text:$1.text+''+$2, type: $1.type};}
|
||||
{ $$={text:$text.text+''+$textToken, type: $text.type};}
|
||||
| STR
|
||||
{ $$={text: $1, type: 'text'};}
|
||||
{ $$ = {text: $STR, type: 'string'};}
|
||||
| MD_STR
|
||||
{ $$={text: $1, type: 'markdown'};}
|
||||
{ $$={text: $MD_STR, type: 'markdown'};}
|
||||
;
|
||||
|
||||
|
||||
@@ -456,109 +464,104 @@ keywords
|
||||
|
||||
|
||||
textNoTags: textNoTagsToken
|
||||
{$$=$1;}
|
||||
{$$={text:$textNoTagsToken, type: 'text'};}
|
||||
| textNoTags textNoTagsToken
|
||||
{$$=$1+''+$2;}
|
||||
{$$={text:$textNoTags.text+''+$textNoTagsToken, type: $textNoTags.type};}
|
||||
| STR
|
||||
{ $$={text: $STR, type: 'text'};}
|
||||
| MD_STR
|
||||
{ $$={text: $MD_STR, type: 'markdown'};}
|
||||
;
|
||||
|
||||
|
||||
classDefStatement:CLASSDEF SPACE DEFAULT SPACE stylesOpt
|
||||
{$$ = $1;yy.addClass($3,$5);}
|
||||
| CLASSDEF SPACE alphaNum SPACE stylesOpt
|
||||
{$$ = $1;yy.addClass($3,$5);}
|
||||
classDefStatement:CLASSDEF SPACE idString SPACE stylesOpt
|
||||
{$$ = $CLASSDEF;yy.addClass($idString,$stylesOpt);}
|
||||
;
|
||||
|
||||
classStatement:CLASS SPACE alphaNum SPACE alphaNum
|
||||
{$$ = $1;yy.setClass($3, $5);}
|
||||
classStatement:CLASS SPACE idString\[vertex] SPACE idString\[class]
|
||||
{$$ = $CLASS;yy.setClass($vertex, $class);}
|
||||
;
|
||||
|
||||
clickStatement
|
||||
: CLICK CALLBACKNAME {$$ = $1;yy.setClickEvent($1, $2);}
|
||||
| CLICK CALLBACKNAME SPACE STR {$$ = $1;yy.setClickEvent($1, $2);yy.setTooltip($1, $4);}
|
||||
| CLICK CALLBACKNAME CALLBACKARGS {$$ = $1;yy.setClickEvent($1, $2, $3);}
|
||||
| CLICK CALLBACKNAME CALLBACKARGS SPACE STR {$$ = $1;yy.setClickEvent($1, $2, $3);yy.setTooltip($1, $5);}
|
||||
| CLICK HREF {$$ = $1;yy.setLink($1, $2);}
|
||||
| CLICK HREF SPACE STR {$$ = $1;yy.setLink($1, $2);yy.setTooltip($1, $4);}
|
||||
| CLICK HREF SPACE LINK_TARGET {$$ = $1;yy.setLink($1, $2, $4);}
|
||||
| CLICK HREF SPACE STR SPACE LINK_TARGET {$$ = $1;yy.setLink($1, $2, $6);yy.setTooltip($1, $4);}
|
||||
| CLICK alphaNum {$$ = $1;yy.setClickEvent($1, $2);}
|
||||
| CLICK alphaNum SPACE STR {$$ = $1;yy.setClickEvent($1, $2);yy.setTooltip($1, $4);}
|
||||
| CLICK STR {$$ = $1;yy.setLink($1, $2);}
|
||||
| CLICK STR SPACE STR {$$ = $1;yy.setLink($1, $2);yy.setTooltip($1, $4);}
|
||||
| CLICK STR SPACE LINK_TARGET {$$ = $1;yy.setLink($1, $2, $4);}
|
||||
| CLICK STR SPACE STR SPACE LINK_TARGET {$$ = $1;yy.setLink($1, $2, $6);yy.setTooltip($1, $4);}
|
||||
: CLICK CALLBACKNAME {$$ = $CLICK;yy.setClickEvent($CLICK, $CALLBACKNAME);}
|
||||
| CLICK CALLBACKNAME SPACE STR {$$ = $CLICK;yy.setClickEvent($CLICK, $CALLBACKNAME);yy.setTooltip($CLICK, $STR);}
|
||||
| CLICK CALLBACKNAME CALLBACKARGS {$$ = $CLICK;yy.setClickEvent($CLICK, $CALLBACKNAME, $CALLBACKARGS);}
|
||||
| CLICK CALLBACKNAME CALLBACKARGS SPACE STR {$$ = $CLICK;yy.setClickEvent($CLICK, $CALLBACKNAME, $CALLBACKARGS);yy.setTooltip($CLICK, $STR);}
|
||||
| CLICK HREF STR {$$ = $CLICK;yy.setLink($CLICK, $STR);}
|
||||
| CLICK HREF STR SPACE STR {$$ = $CLICK;yy.setLink($CLICK, $STR1);yy.setTooltip($CLICK, $STR2);}
|
||||
| CLICK HREF STR SPACE LINK_TARGET {$$ = $CLICK;yy.setLink($CLICK, $STR, $LINK_TARGET);}
|
||||
| CLICK HREF STR\[link] SPACE STR\[tooltip] SPACE LINK_TARGET {$$ = $CLICK;yy.setLink($CLICK, $link, $LINK_TARGET);yy.setTooltip($CLICK, $tooltip);}
|
||||
| CLICK alphaNum {$$ = $CLICK;yy.setClickEvent($CLICK, $alphaNum);}
|
||||
| CLICK alphaNum SPACE STR {$$ = $CLICK;yy.setClickEvent($CLICK, $alphaNum);yy.setTooltip($CLICK, $STR);}
|
||||
| CLICK STR {$$ = $CLICK;yy.setLink($CLICK, $STR);}
|
||||
| CLICK STR\[link] SPACE STR\[tooltip] {$$ = $CLICK;yy.setLink($CLICK, $link);yy.setTooltip($CLICK, $tooltip);}
|
||||
| CLICK STR SPACE LINK_TARGET {$$ = $CLICK;yy.setLink($CLICK, $STR, $LINK_TARGET);}
|
||||
| CLICK STR\[link] SPACE STR\[tooltip] SPACE LINK_TARGET {$$ = $CLICK;yy.setLink($CLICK, $link, $LINK_TARGET);yy.setTooltip($CLICK, $tooltip);}
|
||||
;
|
||||
|
||||
styleStatement:STYLE SPACE alphaNum SPACE stylesOpt
|
||||
{$$ = $1;yy.addVertex($3,undefined,undefined,$5);}
|
||||
| STYLE SPACE HEX SPACE stylesOpt
|
||||
{$$ = $1;yy.updateLink($3,$5);}
|
||||
styleStatement:STYLE SPACE idString SPACE stylesOpt
|
||||
{$$ = $STYLE;yy.addVertex($idString,undefined,undefined,$stylesOpt);}
|
||||
;
|
||||
|
||||
linkStyleStatement
|
||||
: LINKSTYLE SPACE DEFAULT SPACE stylesOpt
|
||||
{$$ = $1;yy.updateLink([$3],$5);}
|
||||
{$$ = $LINKSTYLE;yy.updateLink([$DEFAULT],$stylesOpt);}
|
||||
| LINKSTYLE SPACE numList SPACE stylesOpt
|
||||
{$$ = $1;yy.updateLink($3,$5);}
|
||||
{$$ = $LINKSTYLE;yy.updateLink($numList,$stylesOpt);}
|
||||
| LINKSTYLE SPACE DEFAULT SPACE INTERPOLATE SPACE alphaNum SPACE stylesOpt
|
||||
{$$ = $1;yy.updateLinkInterpolate([$3],$7);yy.updateLink([$3],$9);}
|
||||
{$$ = $LINKSTYLE;yy.updateLinkInterpolate([$DEFAULT],$alphaNum);yy.updateLink([$DEFAULT],$stylesOpt);}
|
||||
| LINKSTYLE SPACE numList SPACE INTERPOLATE SPACE alphaNum SPACE stylesOpt
|
||||
{$$ = $1;yy.updateLinkInterpolate($3,$7);yy.updateLink($3,$9);}
|
||||
{$$ = $LINKSTYLE;yy.updateLinkInterpolate($numList,$alphaNum);yy.updateLink($numList,$stylesOpt);}
|
||||
| LINKSTYLE SPACE DEFAULT SPACE INTERPOLATE SPACE alphaNum
|
||||
{$$ = $1;yy.updateLinkInterpolate([$3],$7);}
|
||||
{$$ = $LINKSTYLE;yy.updateLinkInterpolate([$DEFAULT],$alphaNum);}
|
||||
| LINKSTYLE SPACE numList SPACE INTERPOLATE SPACE alphaNum
|
||||
{$$ = $1;yy.updateLinkInterpolate($3,$7);}
|
||||
{$$ = $LINKSTYLE;yy.updateLinkInterpolate($numList,$alphaNum);}
|
||||
;
|
||||
|
||||
numList: NUM
|
||||
{$$ = [$1]}
|
||||
{$$ = [$NUM]}
|
||||
| numList COMMA NUM
|
||||
{$1.push($3);$$ = $1;}
|
||||
{$numList.push($NUM);$$ = $numList;}
|
||||
;
|
||||
|
||||
stylesOpt: style
|
||||
{$$ = [$1]}
|
||||
{$$ = [$style]}
|
||||
| stylesOpt COMMA style
|
||||
{$1.push($3);$$ = $1;}
|
||||
{$stylesOpt.push($style);$$ = $stylesOpt;}
|
||||
;
|
||||
|
||||
style: styleComponent
|
||||
|style styleComponent
|
||||
{$$ = $1 + $2;}
|
||||
{$$ = $style + $styleComponent;}
|
||||
;
|
||||
|
||||
styleComponent: ALPHA | COLON | MINUS | NUM | UNIT | SPACE | HEX | BRKT | DOT | STYLE | PCT ;
|
||||
styleComponent: NUM | NODE_STRING| COLON | UNIT | SPACE | BRKT | STYLE | PCT ;
|
||||
|
||||
/* Token lists */
|
||||
idStringToken : NUM | NODE_STRING | DOWN | MINUS | DEFAULT | COMMA | COLON | AMP | BRKT | MULT | UNICODE_TEXT;
|
||||
|
||||
textToken : textNoTagsToken | TAGSTART | TAGEND | START_LINK | PCT | DEFAULT;
|
||||
textToken : TEXT | TAGSTART | TAGEND | UNICODE_TEXT;
|
||||
|
||||
textNoTagsToken: alphaNumToken | SPACE | MINUS | keywords ;
|
||||
textNoTagsToken: NUM | NODE_STRING | SPACE | MINUS | AMP | UNICODE_TEXT | COLON | MULT | BRKT | keywords | START_LINK ;
|
||||
|
||||
edgeTextToken : EDGE_TEXT | UNICODE_TEXT ;
|
||||
|
||||
alphaNumToken : NUM | UNICODE_TEXT | NODE_STRING | DIR | DOWN | MINUS | COMMA | COLON | AMP | BRKT | MULT;
|
||||
|
||||
idString
|
||||
:idStringToken
|
||||
{$$=$1}
|
||||
{$$=$idStringToken}
|
||||
| idString idStringToken
|
||||
{$$=$1+''+$2}
|
||||
{$$=$idString+''+$idStringToken}
|
||||
;
|
||||
|
||||
alphaNum
|
||||
: alphaNumStatement
|
||||
{$$=$1;}
|
||||
| alphaNum alphaNumStatement
|
||||
{$$=$1+''+$2;}
|
||||
: alphaNumToken
|
||||
{$$=$alphaNumToken;}
|
||||
| alphaNum alphaNumToken
|
||||
{$$=$alphaNum+''+$alphaNumToken;}
|
||||
;
|
||||
|
||||
alphaNumStatement
|
||||
: DIR
|
||||
{$$=$1;}
|
||||
| alphaNumToken
|
||||
{$$=$1;}
|
||||
| DOWN
|
||||
{$$='v';}
|
||||
| MINUS
|
||||
{$$='-';}
|
||||
;
|
||||
|
||||
direction
|
||||
: direction_tb
|
||||
@@ -571,9 +574,4 @@ direction
|
||||
{ $$={stmt:'dir', value:'LR'};}
|
||||
;
|
||||
|
||||
alphaNumToken : PUNCTUATION | AMP | UNICODE_TEXT | NUM| ALPHA | COLON | COMMA | PLUS | EQUALS | MULT | DOT | BRKT| UNDERSCORE ;
|
||||
|
||||
idStringToken : ALPHA|UNDERSCORE |UNICODE_TEXT | NUM| COLON | COMMA | PLUS | MINUS | DOWN |EQUALS | MULT | BRKT | DOT | PUNCTUATION | AMP | DEFAULT;
|
||||
|
||||
graphCodeTokens: STADIUMSTART | STADIUMEND | SUBROUTINESTART | SUBROUTINEEND | VERTEX_WITH_PROPS_START | CYLINDERSTART | CYLINDEREND | TRAPSTART | TRAPEND | INVTRAPSTART | INVTRAPEND | PIPE | PS | PE | SQS | SQE | DIAMOND_START | DIAMOND_STOP | TAGSTART | TAGEND | ARROW_CROSS | ARROW_POINT | ARROW_CIRCLE | ARROW_OPEN | QUOTE | SEMI;
|
||||
%%
|
||||
|
||||
@@ -6,7 +6,6 @@ import dayjsAdvancedFormat from 'dayjs/plugin/advancedFormat.js';
|
||||
import { log } from '../../logger.js';
|
||||
import * as configApi from '../../config.js';
|
||||
import utils from '../../utils.js';
|
||||
import mermaidAPI from '../../mermaidAPI.js';
|
||||
|
||||
import {
|
||||
setAccTitle,
|
||||
@@ -16,7 +15,7 @@ import {
|
||||
clear as commonClear,
|
||||
setDiagramTitle,
|
||||
getDiagramTitle,
|
||||
} from '../../commonDb.js';
|
||||
} from '../common/commonDb.js';
|
||||
|
||||
dayjs.extend(dayjsIsoWeek);
|
||||
dayjs.extend(dayjsCustomParseFormat);
|
||||
@@ -37,14 +36,11 @@ const tags = ['active', 'done', 'crit', 'milestone'];
|
||||
let funs = [];
|
||||
let inclusiveEndDates = false;
|
||||
let topAxis = false;
|
||||
let weekday = 'sunday';
|
||||
|
||||
// The serial order of the task in the script
|
||||
let lastOrder = 0;
|
||||
|
||||
export const parseDirective = function (statement, context, type) {
|
||||
mermaidAPI.parseDirective(this, statement, context, type);
|
||||
};
|
||||
|
||||
export const clear = function () {
|
||||
sections = [];
|
||||
tasks = [];
|
||||
@@ -66,6 +62,7 @@ export const clear = function () {
|
||||
lastOrder = 0;
|
||||
links = {};
|
||||
commonClear();
|
||||
weekday = 'sunday';
|
||||
};
|
||||
|
||||
export const setAxisFormat = function (txt) {
|
||||
@@ -179,6 +176,14 @@ export const isInvalidDate = function (date, dateFormat, excludes, includes) {
|
||||
return excludes.includes(date.format(dateFormat.trim()));
|
||||
};
|
||||
|
||||
export const setWeekday = function (txt) {
|
||||
weekday = txt;
|
||||
};
|
||||
|
||||
export const getWeekday = function () {
|
||||
return weekday;
|
||||
};
|
||||
|
||||
/**
|
||||
* TODO: fully document what this function does and what types it accepts
|
||||
*
|
||||
@@ -720,7 +725,6 @@ export const bindFunctions = function (element) {
|
||||
};
|
||||
|
||||
export default {
|
||||
parseDirective,
|
||||
getConfig: () => configApi.getConfig().gantt,
|
||||
clear,
|
||||
setDateFormat,
|
||||
@@ -759,6 +763,8 @@ export default {
|
||||
bindFunctions,
|
||||
parseDuration,
|
||||
isInvalidDate,
|
||||
setWeekday,
|
||||
getWeekday,
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// @ts-ignore: TODO Fix ts errors
|
||||
// @ts-ignore: JISON doesn't support types
|
||||
import ganttParser from './parser/gantt.jison';
|
||||
import ganttDb from './ganttDb.js';
|
||||
import ganttRenderer from './ganttRenderer.js';
|
||||
import ganttStyles from './styles.js';
|
||||
import { DiagramDefinition } from '../../diagram-api/types.js';
|
||||
import type { DiagramDefinition } from '../../diagram-api/types.js';
|
||||
|
||||
export const diagram: DiagramDefinition = {
|
||||
parser: ganttParser,
|
||||
|
||||
@@ -10,10 +10,18 @@ import {
|
||||
axisBottom,
|
||||
axisTop,
|
||||
timeFormat,
|
||||
timeMillisecond,
|
||||
timeSecond,
|
||||
timeMinute,
|
||||
timeHour,
|
||||
timeDay,
|
||||
timeWeek,
|
||||
timeMonday,
|
||||
timeTuesday,
|
||||
timeWednesday,
|
||||
timeThursday,
|
||||
timeFriday,
|
||||
timeSaturday,
|
||||
timeSunday,
|
||||
timeMonth,
|
||||
} from 'd3';
|
||||
import common from '../common/common.js';
|
||||
@@ -24,6 +32,20 @@ export const setConf = function () {
|
||||
log.debug('Something is calling, setConf, remove the call');
|
||||
};
|
||||
|
||||
/**
|
||||
* This will map any day of the week that can be set in the `weekday` option to
|
||||
* the corresponding d3-time function that is used to calculate the ticks.
|
||||
*/
|
||||
const mapWeekdayToTimeFunction = {
|
||||
monday: timeMonday,
|
||||
tuesday: timeTuesday,
|
||||
wednesday: timeWednesday,
|
||||
thursday: timeThursday,
|
||||
friday: timeFriday,
|
||||
saturday: timeSaturday,
|
||||
sunday: timeSunday,
|
||||
};
|
||||
|
||||
/**
|
||||
* For this issue:
|
||||
* https://github.com/mermaid-js/mermaid/issues/1618
|
||||
@@ -475,20 +497,37 @@ export const draw = function (text, id, version, diagObj) {
|
||||
* @param w
|
||||
* @param h
|
||||
* @param tasks
|
||||
* @param excludes
|
||||
* @param includes
|
||||
* @param {unknown[]} excludes
|
||||
* @param {unknown[]} includes
|
||||
*/
|
||||
function drawExcludeDays(theGap, theTopPad, theSidePad, w, h, tasks, excludes, includes) {
|
||||
const minTime = tasks.reduce(
|
||||
(min, { startTime }) => (min ? Math.min(min, startTime) : startTime),
|
||||
0
|
||||
);
|
||||
const maxTime = tasks.reduce((max, { endTime }) => (max ? Math.max(max, endTime) : endTime), 0);
|
||||
const dateFormat = diagObj.db.getDateFormat();
|
||||
if (excludes.length === 0 && includes.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let minTime;
|
||||
let maxTime;
|
||||
for (const { startTime, endTime } of tasks) {
|
||||
if (minTime === undefined || startTime < minTime) {
|
||||
minTime = startTime;
|
||||
}
|
||||
if (maxTime === undefined || endTime > maxTime) {
|
||||
maxTime = endTime;
|
||||
}
|
||||
}
|
||||
|
||||
if (!minTime || !maxTime) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (dayjs(maxTime).diff(dayjs(minTime), 'year') > 5) {
|
||||
log.warn(
|
||||
'The difference between the min and max time is more than 5 years. This will cause performance issues. Skipping drawing exclude days.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const dateFormat = diagObj.db.getDateFormat();
|
||||
const excludeRanges = [];
|
||||
let range = null;
|
||||
let d = dayjs(minTime);
|
||||
@@ -553,7 +592,7 @@ export const draw = function (text, id, version, diagObj) {
|
||||
.tickSize(-h + theTopPad + conf.gridLineStartPadding)
|
||||
.tickFormat(timeFormat(diagObj.db.getAxisFormat() || conf.axisFormat || '%Y-%m-%d'));
|
||||
|
||||
const reTickInterval = /^([1-9]\d*)(minute|hour|day|week|month)$/;
|
||||
const reTickInterval = /^([1-9]\d*)(millisecond|second|minute|hour|day|week|month)$/;
|
||||
const resultTickInterval = reTickInterval.exec(
|
||||
diagObj.db.getTickInterval() || conf.tickInterval
|
||||
);
|
||||
@@ -561,7 +600,15 @@ export const draw = function (text, id, version, diagObj) {
|
||||
if (resultTickInterval !== null) {
|
||||
const every = resultTickInterval[1];
|
||||
const interval = resultTickInterval[2];
|
||||
const weekday = diagObj.db.getWeekday() || conf.weekday;
|
||||
|
||||
switch (interval) {
|
||||
case 'millisecond':
|
||||
bottomXAxis.ticks(timeMillisecond.every(every));
|
||||
break;
|
||||
case 'second':
|
||||
bottomXAxis.ticks(timeSecond.every(every));
|
||||
break;
|
||||
case 'minute':
|
||||
bottomXAxis.ticks(timeMinute.every(every));
|
||||
break;
|
||||
@@ -572,7 +619,7 @@ export const draw = function (text, id, version, diagObj) {
|
||||
bottomXAxis.ticks(timeDay.every(every));
|
||||
break;
|
||||
case 'week':
|
||||
bottomXAxis.ticks(timeWeek.every(every));
|
||||
bottomXAxis.ticks(mapWeekdayToTimeFunction[weekday].every(every));
|
||||
break;
|
||||
case 'month':
|
||||
bottomXAxis.ticks(timeMonth.every(every));
|
||||
@@ -600,7 +647,15 @@ export const draw = function (text, id, version, diagObj) {
|
||||
if (resultTickInterval !== null) {
|
||||
const every = resultTickInterval[1];
|
||||
const interval = resultTickInterval[2];
|
||||
const weekday = diagObj.db.getWeekday() || conf.weekday;
|
||||
|
||||
switch (interval) {
|
||||
case 'millisecond':
|
||||
topXAxis.ticks(timeMillisecond.every(every));
|
||||
break;
|
||||
case 'second':
|
||||
topXAxis.ticks(timeSecond.every(every));
|
||||
break;
|
||||
case 'minute':
|
||||
topXAxis.ticks(timeMinute.every(every));
|
||||
break;
|
||||
@@ -611,7 +666,7 @@ export const draw = function (text, id, version, diagObj) {
|
||||
topXAxis.ticks(timeDay.every(every));
|
||||
break;
|
||||
case 'week':
|
||||
topXAxis.ticks(timeWeek.every(every));
|
||||
topXAxis.ticks(mapWeekdayToTimeFunction[weekday].every(every));
|
||||
break;
|
||||
case 'month':
|
||||
topXAxis.ticks(timeMonth.every(every));
|
||||
|
||||
@@ -11,19 +11,11 @@
|
||||
%x href
|
||||
%x callbackname
|
||||
%x callbackargs
|
||||
%x open_directive
|
||||
%x type_directive
|
||||
%x arg_directive
|
||||
%x close_directive
|
||||
%x acc_title
|
||||
%x acc_descr
|
||||
%x acc_descr_multiline
|
||||
%%
|
||||
\%\%\{ { this.begin('open_directive'); return 'open_directive'; }
|
||||
<open_directive>((?:(?!\}\%\%)[^:.])*) { this.begin('type_directive'); return 'type_directive'; }
|
||||
<type_directive>":" { this.popState(); this.begin('arg_directive'); return ':'; }
|
||||
<type_directive,arg_directive>\}\%\% { this.popState(); this.popState(); return 'close_directive'; }
|
||||
<arg_directive>((?:(?!\}\%\%).|\n)*) return 'arg_directive';
|
||||
|
||||
accTitle\s*":"\s* { this.begin("acc_title");return 'acc_title'; }
|
||||
<acc_title>(?!\n|;|#)*[^\n]* { this.popState(); return "acc_title_value"; }
|
||||
@@ -77,24 +69,31 @@ that id.
|
||||
<click>[\s\n] this.popState();
|
||||
<click>[^\s\n]* return 'click';
|
||||
|
||||
"gantt" return 'gantt';
|
||||
"dateFormat"\s[^#\n;]+ return 'dateFormat';
|
||||
"inclusiveEndDates" return 'inclusiveEndDates';
|
||||
"topAxis" return 'topAxis';
|
||||
"axisFormat"\s[^#\n;]+ return 'axisFormat';
|
||||
"tickInterval"\s[^#\n;]+ return 'tickInterval';
|
||||
"includes"\s[^#\n;]+ return 'includes';
|
||||
"excludes"\s[^#\n;]+ return 'excludes';
|
||||
"todayMarker"\s[^\n;]+ return 'todayMarker';
|
||||
\d\d\d\d"-"\d\d"-"\d\d return 'date';
|
||||
"title"\s[^#\n;]+ return 'title';
|
||||
"accDescription"\s[^#\n;]+ return 'accDescription'
|
||||
"section"\s[^#:\n;]+ return 'section';
|
||||
[^#:\n;]+ return 'taskTxt';
|
||||
":"[^#\n;]+ return 'taskData';
|
||||
":" return ':';
|
||||
<<EOF>> return 'EOF';
|
||||
. return 'INVALID';
|
||||
"gantt" return 'gantt';
|
||||
"dateFormat"\s[^#\n;]+ return 'dateFormat';
|
||||
"inclusiveEndDates" return 'inclusiveEndDates';
|
||||
"topAxis" return 'topAxis';
|
||||
"axisFormat"\s[^#\n;]+ return 'axisFormat';
|
||||
"tickInterval"\s[^#\n;]+ return 'tickInterval';
|
||||
"includes"\s[^#\n;]+ return 'includes';
|
||||
"excludes"\s[^#\n;]+ return 'excludes';
|
||||
"todayMarker"\s[^\n;]+ return 'todayMarker';
|
||||
weekday\s+monday return 'weekday_monday'
|
||||
weekday\s+tuesday return 'weekday_tuesday'
|
||||
weekday\s+wednesday return 'weekday_wednesday'
|
||||
weekday\s+thursday return 'weekday_thursday'
|
||||
weekday\s+friday return 'weekday_friday'
|
||||
weekday\s+saturday return 'weekday_saturday'
|
||||
weekday\s+sunday return 'weekday_sunday'
|
||||
\d\d\d\d"-"\d\d"-"\d\d return 'date';
|
||||
"title"\s[^#\n;]+ return 'title';
|
||||
"accDescription"\s[^#\n;]+ return 'accDescription'
|
||||
"section"\s[^#:\n;]+ return 'section';
|
||||
[^#:\n;]+ return 'taskTxt';
|
||||
":"[^#\n;]+ return 'taskData';
|
||||
":" return ':';
|
||||
<<EOF>> return 'EOF';
|
||||
. return 'INVALID';
|
||||
|
||||
/lex
|
||||
|
||||
@@ -105,8 +104,7 @@ that id.
|
||||
%% /* language grammar */
|
||||
|
||||
start
|
||||
: directive start
|
||||
| gantt document 'EOF' { return $2; }
|
||||
: gantt document 'EOF' { return $2; }
|
||||
;
|
||||
|
||||
document
|
||||
@@ -121,6 +119,16 @@ line
|
||||
| EOF { $$=[];}
|
||||
;
|
||||
|
||||
weekday
|
||||
: weekday_monday { yy.setWeekday("monday");}
|
||||
| weekday_tuesday { yy.setWeekday("tuesday");}
|
||||
| weekday_wednesday { yy.setWeekday("wednesday");}
|
||||
| weekday_thursday { yy.setWeekday("thursday");}
|
||||
| weekday_friday { yy.setWeekday("friday");}
|
||||
| weekday_saturday { yy.setWeekday("saturday");}
|
||||
| weekday_sunday { yy.setWeekday("sunday");}
|
||||
;
|
||||
|
||||
statement
|
||||
: dateFormat {yy.setDateFormat($1.substr(11));$$=$1.substr(11);}
|
||||
| inclusiveEndDates {yy.enableInclusiveEndDates();$$=$1.substr(18);}
|
||||
@@ -130,6 +138,7 @@ statement
|
||||
| excludes {yy.setExcludes($1.substr(9));$$=$1.substr(9);}
|
||||
| includes {yy.setIncludes($1.substr(9));$$=$1.substr(9);}
|
||||
| todayMarker {yy.setTodayMarker($1.substr(12));$$=$1.substr(12);}
|
||||
| weekday
|
||||
| title {yy.setDiagramTitle($1.substr(6));$$=$1.substr(6);}
|
||||
| acc_title acc_title_value { $$=$2.trim();yy.setAccTitle($$); }
|
||||
| acc_descr acc_descr_value { $$=$2.trim();yy.setAccDescription($$); }
|
||||
@@ -137,13 +146,8 @@ statement
|
||||
| section { yy.addSection($1.substr(8));$$=$1.substr(8); }
|
||||
| clickStatement
|
||||
| taskTxt taskData {yy.addTask($1,$2);$$='task';}
|
||||
| directive
|
||||
;
|
||||
|
||||
directive
|
||||
: openDirective typeDirective closeDirective 'NL'
|
||||
| openDirective typeDirective ':' argDirective closeDirective 'NL'
|
||||
;
|
||||
|
||||
/*
|
||||
click allows any combination of href and call.
|
||||
@@ -174,20 +178,4 @@ clickStatementDebug
|
||||
| click href {$$=$1 + ' ' + $2;}
|
||||
;
|
||||
|
||||
openDirective
|
||||
: open_directive { yy.parseDirective('%%{', 'open_directive'); }
|
||||
;
|
||||
|
||||
typeDirective
|
||||
: type_directive { yy.parseDirective($1, 'type_directive'); }
|
||||
;
|
||||
|
||||
argDirective
|
||||
: arg_directive { $1 = $1.trim().replace(/'/g, '"'); yy.parseDirective($1, 'arg_directive'); }
|
||||
;
|
||||
|
||||
closeDirective
|
||||
: close_directive { yy.parseDirective('}%%', 'close_directive', 'gantt'); }
|
||||
;
|
||||
|
||||
%%
|
||||
|
||||
@@ -180,4 +180,12 @@ row2`;
|
||||
expect(ganttDb.getAccTitle()).toBe(expectedTitle);
|
||||
expect(ganttDb.getAccDescription()).toBe(expectedAccDescription);
|
||||
});
|
||||
|
||||
it.each(['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'])(
|
||||
'should allow for setting the starting weekday to %s for tick interval',
|
||||
(day) => {
|
||||
parser.parse(`gantt\nweekday ${day}`);
|
||||
expect(ganttDb.getWeekday()).toBe(day);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { log } from '../../logger.js';
|
||||
import { random } from '../../utils.js';
|
||||
import mermaidAPI from '../../mermaidAPI.js';
|
||||
import * as configApi from '../../config.js';
|
||||
import { getConfig } from '../../config.js';
|
||||
import common from '../common/common.js';
|
||||
@@ -12,7 +11,7 @@ import {
|
||||
clear as commonClear,
|
||||
setDiagramTitle,
|
||||
getDiagramTitle,
|
||||
} from '../../commonDb.js';
|
||||
} from '../common/commonDb.js';
|
||||
|
||||
let mainBranchName = getConfig().gitGraph.mainBranchName;
|
||||
let mainBranchOrder = getConfig().gitGraph.mainBranchOrder;
|
||||
@@ -33,10 +32,6 @@ function getId() {
|
||||
return random({ length: 7 });
|
||||
}
|
||||
|
||||
export const parseDirective = function (statement, context, type) {
|
||||
mermaidAPI.parseDirective(this, statement, context, type);
|
||||
};
|
||||
|
||||
// /**
|
||||
// * @param currentCommit
|
||||
// * @param otherCommit
|
||||
@@ -507,7 +502,6 @@ export const commitType = {
|
||||
};
|
||||
|
||||
export default {
|
||||
parseDirective,
|
||||
getConfig: () => configApi.getConfig().gitGraph,
|
||||
setDirection,
|
||||
setOptions,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// @ts-ignore: TODO Fix ts errors
|
||||
// @ts-ignore: JISON doesn't support types
|
||||
import gitGraphParser from './parser/gitGraph.jison';
|
||||
import gitGraphDb from './gitGraphAst.js';
|
||||
import gitGraphRenderer from './gitGraphRenderer.js';
|
||||
import gitGraphStyles from './styles.js';
|
||||
import { DiagramDefinition } from '../../diagram-api/types.js';
|
||||
import type { DiagramDefinition } from '../../diagram-api/types.js';
|
||||
|
||||
export const diagram: DiagramDefinition = {
|
||||
parser: gitGraphParser,
|
||||
|
||||
@@ -1,87 +1,72 @@
|
||||
import gitGraphAst from './gitGraphAst.js';
|
||||
import { parser } from './parser/gitGraph.jison';
|
||||
|
||||
// Todo reintroduce without cryptoRandomString
|
||||
import gitGraphAst from './gitGraphAst';
|
||||
import { parser } from './parser/gitGraph';
|
||||
import randomString from 'crypto-random-string';
|
||||
import cryptoRandomString from 'crypto-random-string';
|
||||
|
||||
jest.mock('crypto-random-string');
|
||||
|
||||
describe('when parsing a gitGraph', function() {
|
||||
let randomNumber;
|
||||
beforeEach(function() {
|
||||
describe('when parsing a gitGraph', function () {
|
||||
beforeEach(function () {
|
||||
parser.yy = gitGraphAst;
|
||||
parser.yy.clear();
|
||||
randomNumber = 0;
|
||||
cryptoRandomString.mockImplementation(() => {
|
||||
randomNumber = randomNumber + 1;
|
||||
return String(randomNumber);
|
||||
});
|
||||
});
|
||||
afterEach(function() {
|
||||
cryptoRandomString.mockReset();
|
||||
});
|
||||
it('should handle a gitGraph definition', function() {
|
||||
it('should handle a gitGraph definition', function () {
|
||||
const str = 'gitGraph:\n' + 'commit\n';
|
||||
|
||||
parser.parse(str);
|
||||
const commits = parser.yy.getCommits();
|
||||
|
||||
expect(Object.keys(commits).length).toBe(1);
|
||||
expect(parser.yy.getCurrentBranch()).toBe('master');
|
||||
expect(parser.yy.getCurrentBranch()).toBe('main');
|
||||
expect(parser.yy.getDirection()).toBe('LR');
|
||||
expect(Object.keys(parser.yy.getBranches()).length).toBe(1);
|
||||
});
|
||||
|
||||
it('should handle a gitGraph definition with empty options', function() {
|
||||
const str = 'gitGraph:\n' + 'options\n' + 'end\n' + 'commit\n';
|
||||
it('should handle a gitGraph definition with empty options', function () {
|
||||
const str = 'gitGraph:\n' + 'options\n' + ' end\n' + 'commit\n';
|
||||
|
||||
parser.parse(str);
|
||||
const commits = parser.yy.getCommits();
|
||||
|
||||
expect(parser.yy.getOptions()).toEqual({});
|
||||
expect(Object.keys(commits).length).toBe(1);
|
||||
expect(parser.yy.getCurrentBranch()).toBe('master');
|
||||
expect(parser.yy.getCurrentBranch()).toBe('main');
|
||||
expect(parser.yy.getDirection()).toBe('LR');
|
||||
expect(Object.keys(parser.yy.getBranches()).length).toBe(1);
|
||||
});
|
||||
|
||||
it('should handle a gitGraph definition with valid options', function() {
|
||||
it('should handle a gitGraph definition with valid options', function () {
|
||||
const str = 'gitGraph:\n' + 'options\n' + '{"key": "value"}\n' + 'end\n' + 'commit\n';
|
||||
|
||||
parser.parse(str);
|
||||
const commits = parser.yy.getCommits();
|
||||
expect(parser.yy.getOptions()['key']).toBe('value');
|
||||
expect(Object.keys(commits).length).toBe(1);
|
||||
expect(parser.yy.getCurrentBranch()).toBe('master');
|
||||
expect(parser.yy.getCurrentBranch()).toBe('main');
|
||||
expect(parser.yy.getDirection()).toBe('LR');
|
||||
expect(Object.keys(parser.yy.getBranches()).length).toBe(1);
|
||||
});
|
||||
|
||||
it('should not fail on a gitGraph with malformed json', function() {
|
||||
it('should not fail on a gitGraph with malformed json', function () {
|
||||
const str = 'gitGraph:\n' + 'options\n' + '{"key": "value"\n' + 'end\n' + 'commit\n';
|
||||
|
||||
parser.parse(str);
|
||||
const commits = parser.yy.getCommits();
|
||||
expect(Object.keys(commits).length).toBe(1);
|
||||
expect(parser.yy.getCurrentBranch()).toBe('master');
|
||||
expect(parser.yy.getCurrentBranch()).toBe('main');
|
||||
expect(parser.yy.getDirection()).toBe('LR');
|
||||
expect(Object.keys(parser.yy.getBranches()).length).toBe(1);
|
||||
});
|
||||
|
||||
it('should handle set direction', function() {
|
||||
const str = 'gitGraph BT:\n' + 'commit\n';
|
||||
it('should handle set direction', function () {
|
||||
const str = 'gitGraph TB:\n' + 'commit\n';
|
||||
|
||||
parser.parse(str);
|
||||
const commits = parser.yy.getCommits();
|
||||
|
||||
expect(Object.keys(commits).length).toBe(1);
|
||||
expect(parser.yy.getCurrentBranch()).toBe('master');
|
||||
expect(parser.yy.getDirection()).toBe('BT');
|
||||
expect(parser.yy.getCurrentBranch()).toBe('main');
|
||||
expect(parser.yy.getDirection()).toBe('TB');
|
||||
expect(Object.keys(parser.yy.getBranches()).length).toBe(1);
|
||||
});
|
||||
|
||||
it('should checkout a branch', function() {
|
||||
it('should checkout a branch', function () {
|
||||
const str = 'gitGraph:\n' + 'branch new\n' + 'checkout new\n';
|
||||
|
||||
parser.parse(str);
|
||||
@@ -91,7 +76,7 @@ describe('when parsing a gitGraph', function() {
|
||||
expect(parser.yy.getCurrentBranch()).toBe('new');
|
||||
});
|
||||
|
||||
it('should add commits to checked out branch', function() {
|
||||
it('should add commits to checked out branch', function () {
|
||||
const str = 'gitGraph:\n' + 'branch new\n' + 'checkout new\n' + 'commit\n' + 'commit\n';
|
||||
|
||||
parser.parse(str);
|
||||
@@ -103,7 +88,7 @@ describe('when parsing a gitGraph', function() {
|
||||
expect(branchCommit).not.toBeNull();
|
||||
expect(commits[branchCommit].parent).not.toBeNull();
|
||||
});
|
||||
it('should handle commit with args', function() {
|
||||
it('should handle commit with args', function () {
|
||||
const str = 'gitGraph:\n' + 'commit "a commit"\n';
|
||||
|
||||
parser.parse(str);
|
||||
@@ -112,10 +97,11 @@ describe('when parsing a gitGraph', function() {
|
||||
expect(Object.keys(commits).length).toBe(1);
|
||||
const key = Object.keys(commits)[0];
|
||||
expect(commits[key].message).toBe('a commit');
|
||||
expect(parser.yy.getCurrentBranch()).toBe('master');
|
||||
expect(parser.yy.getCurrentBranch()).toBe('main');
|
||||
});
|
||||
|
||||
it('should reset a branch', function() {
|
||||
// Reset has been commented out in JISON
|
||||
it.skip('should reset a branch', function () {
|
||||
const str =
|
||||
'gitGraph:\n' +
|
||||
'commit\n' +
|
||||
@@ -123,18 +109,18 @@ describe('when parsing a gitGraph', function() {
|
||||
'branch newbranch\n' +
|
||||
'checkout newbranch\n' +
|
||||
'commit\n' +
|
||||
'reset master\n';
|
||||
'reset main\n';
|
||||
|
||||
parser.parse(str);
|
||||
|
||||
const commits = parser.yy.getCommits();
|
||||
expect(Object.keys(commits).length).toBe(3);
|
||||
expect(parser.yy.getCurrentBranch()).toBe('newbranch');
|
||||
expect(parser.yy.getBranches()['newbranch']).toEqual(parser.yy.getBranches()['master']);
|
||||
expect(parser.yy.getBranches()['newbranch']).toEqual(parser.yy.getBranches()['main']);
|
||||
expect(parser.yy.getHead().id).toEqual(parser.yy.getBranches()['newbranch']);
|
||||
});
|
||||
|
||||
it('reset can take an argument', function() {
|
||||
it.skip('reset can take an argument', function () {
|
||||
const str =
|
||||
'gitGraph:\n' +
|
||||
'commit\n' +
|
||||
@@ -142,18 +128,18 @@ describe('when parsing a gitGraph', function() {
|
||||
'branch newbranch\n' +
|
||||
'checkout newbranch\n' +
|
||||
'commit\n' +
|
||||
'reset master^\n';
|
||||
'reset main^\n';
|
||||
|
||||
parser.parse(str);
|
||||
|
||||
const commits = parser.yy.getCommits();
|
||||
expect(Object.keys(commits).length).toBe(3);
|
||||
expect(parser.yy.getCurrentBranch()).toBe('newbranch');
|
||||
const master = commits[parser.yy.getBranches()['master']];
|
||||
expect(parser.yy.getHead().id).toEqual(master.parent);
|
||||
const main = commits[parser.yy.getBranches()['main']];
|
||||
expect(parser.yy.getHead().id).toEqual(main.parent);
|
||||
});
|
||||
|
||||
it('should handle fast forwardable merges', function() {
|
||||
it.skip('should handle fast forwardable merges', function () {
|
||||
const str =
|
||||
'gitGraph:\n' +
|
||||
'commit\n' +
|
||||
@@ -161,19 +147,19 @@ describe('when parsing a gitGraph', function() {
|
||||
'checkout newbranch\n' +
|
||||
'commit\n' +
|
||||
'commit\n' +
|
||||
'checkout master\n' +
|
||||
'checkout main\n' +
|
||||
'merge newbranch\n';
|
||||
|
||||
parser.parse(str);
|
||||
|
||||
const commits = parser.yy.getCommits();
|
||||
expect(Object.keys(commits).length).toBe(3);
|
||||
expect(parser.yy.getCurrentBranch()).toBe('master');
|
||||
expect(parser.yy.getBranches()['newbranch']).toEqual(parser.yy.getBranches()['master']);
|
||||
expect(Object.keys(commits).length).toBe(4);
|
||||
expect(parser.yy.getCurrentBranch()).toBe('main');
|
||||
expect(parser.yy.getBranches()['newbranch']).toEqual(parser.yy.getBranches()['main']);
|
||||
expect(parser.yy.getHead().id).toEqual(parser.yy.getBranches()['newbranch']);
|
||||
});
|
||||
|
||||
it('should handle cases when merge is a noop', function() {
|
||||
it('should handle cases when merge is a noop', function () {
|
||||
const str =
|
||||
'gitGraph:\n' +
|
||||
'commit\n' +
|
||||
@@ -181,18 +167,18 @@ describe('when parsing a gitGraph', function() {
|
||||
'checkout newbranch\n' +
|
||||
'commit\n' +
|
||||
'commit\n' +
|
||||
'merge master\n';
|
||||
'merge main\n';
|
||||
|
||||
parser.parse(str);
|
||||
|
||||
const commits = parser.yy.getCommits();
|
||||
expect(Object.keys(commits).length).toBe(3);
|
||||
expect(Object.keys(commits).length).toBe(4);
|
||||
expect(parser.yy.getCurrentBranch()).toBe('newbranch');
|
||||
expect(parser.yy.getBranches()['newbranch']).not.toEqual(parser.yy.getBranches()['master']);
|
||||
expect(parser.yy.getBranches()['newbranch']).not.toEqual(parser.yy.getBranches()['main']);
|
||||
expect(parser.yy.getHead().id).toEqual(parser.yy.getBranches()['newbranch']);
|
||||
});
|
||||
|
||||
it('should handle merge with 2 parents', function() {
|
||||
it('should handle merge with 2 parents', function () {
|
||||
const str =
|
||||
'gitGraph:\n' +
|
||||
'commit\n' +
|
||||
@@ -200,7 +186,7 @@ describe('when parsing a gitGraph', function() {
|
||||
'checkout newbranch\n' +
|
||||
'commit\n' +
|
||||
'commit\n' +
|
||||
'checkout master\n' +
|
||||
'checkout main\n' +
|
||||
'commit\n' +
|
||||
'merge newbranch\n';
|
||||
|
||||
@@ -208,12 +194,12 @@ describe('when parsing a gitGraph', function() {
|
||||
|
||||
const commits = parser.yy.getCommits();
|
||||
expect(Object.keys(commits).length).toBe(5);
|
||||
expect(parser.yy.getCurrentBranch()).toBe('master');
|
||||
expect(parser.yy.getBranches()['newbranch']).not.toEqual(parser.yy.getBranches()['master']);
|
||||
expect(parser.yy.getHead().id).toEqual(parser.yy.getBranches()['master']);
|
||||
expect(parser.yy.getCurrentBranch()).toBe('main');
|
||||
expect(parser.yy.getBranches()['newbranch']).not.toEqual(parser.yy.getBranches()['main']);
|
||||
expect(parser.yy.getHead().id).toEqual(parser.yy.getBranches()['main']);
|
||||
});
|
||||
|
||||
it('should handle ff merge when history walk has two parents (merge commit)', function() {
|
||||
it.skip('should handle ff merge when history walk has two parents (merge commit)', function () {
|
||||
const str =
|
||||
'gitGraph:\n' +
|
||||
'commit\n' +
|
||||
@@ -221,53 +207,25 @@ describe('when parsing a gitGraph', function() {
|
||||
'checkout newbranch\n' +
|
||||
'commit\n' +
|
||||
'commit\n' +
|
||||
'checkout master\n' +
|
||||
'checkout main\n' +
|
||||
'commit\n' +
|
||||
'merge newbranch\n' +
|
||||
'commit\n' +
|
||||
'checkout newbranch\n' +
|
||||
'merge master\n';
|
||||
'merge main\n';
|
||||
|
||||
parser.parse(str);
|
||||
|
||||
const commits = parser.yy.getCommits();
|
||||
expect(Object.keys(commits).length).toBe(6);
|
||||
expect(Object.keys(commits).length).toBe(7);
|
||||
expect(parser.yy.getCurrentBranch()).toBe('newbranch');
|
||||
expect(parser.yy.getBranches()['newbranch']).toEqual(parser.yy.getBranches()['master']);
|
||||
expect(parser.yy.getHead().id).toEqual(parser.yy.getBranches()['master']);
|
||||
expect(parser.yy.getBranches()['newbranch']).toEqual(parser.yy.getBranches()['main']);
|
||||
expect(parser.yy.getHead().id).toEqual(parser.yy.getBranches()['main']);
|
||||
|
||||
parser.yy.prettyPrint();
|
||||
});
|
||||
|
||||
it('should generate a secure random ID for commits', function() {
|
||||
const str = 'gitGraph:\n' + 'commit\n' + 'commit\n';
|
||||
const EXPECTED_LENGTH = 7;
|
||||
const EXPECTED_CHARACTERS = '0123456789abcdef';
|
||||
|
||||
let idCount = 0;
|
||||
randomString.mockImplementation(options => {
|
||||
if (
|
||||
options.length === EXPECTED_LENGTH &&
|
||||
options.characters === EXPECTED_CHARACTERS &&
|
||||
Object.keys(options).length === 2
|
||||
) {
|
||||
const id = `abcdef${idCount}`;
|
||||
idCount += 1;
|
||||
return id;
|
||||
}
|
||||
return 'unexpected-ID';
|
||||
});
|
||||
|
||||
parser.parse(str);
|
||||
const commits = parser.yy.getCommits();
|
||||
|
||||
expect(Object.keys(commits)).toEqual(['abcdef0', 'abcdef1']);
|
||||
Object.keys(commits).forEach(key => {
|
||||
expect(commits[key].id).toEqual(key);
|
||||
});
|
||||
});
|
||||
|
||||
it('should generate an array of known branches', function() {
|
||||
it('should generate an array of known branches', function () {
|
||||
const str =
|
||||
'gitGraph:\n' +
|
||||
'commit\n' +
|
||||
@@ -281,7 +239,7 @@ describe('when parsing a gitGraph', function() {
|
||||
const branches = gitGraphAst.getBranchesAsObjArray();
|
||||
|
||||
expect(branches).toHaveLength(3);
|
||||
expect(branches[0]).toHaveProperty('name', 'master');
|
||||
expect(branches[0]).toHaveProperty('name', 'main');
|
||||
expect(branches[1]).toHaveProperty('name', 'b1');
|
||||
expect(branches[2]).toHaveProperty('name', 'b2');
|
||||
});
|
||||
@@ -1,22 +1,11 @@
|
||||
/* eslint-env jasmine */
|
||||
// Todo reintroduce without cryptoRandomString
|
||||
import gitGraphAst from './gitGraphAst.js';
|
||||
import { parser } from './parser/gitGraph.jison';
|
||||
//import randomString from 'crypto-random-string';
|
||||
//import cryptoRandomString from 'crypto-random-string';
|
||||
|
||||
//jest.mock('crypto-random-string');
|
||||
|
||||
describe('when parsing a gitGraph', function () {
|
||||
let randomNumber;
|
||||
beforeEach(function () {
|
||||
parser.yy = gitGraphAst;
|
||||
parser.yy.clear();
|
||||
randomNumber = 0;
|
||||
});
|
||||
// afterEach(function() {
|
||||
// cryptoRandomString.mockReset();
|
||||
// });
|
||||
it('should handle a gitGraph commit with NO pararms, get auto-genrated reandom ID', function () {
|
||||
const str = `gitGraph:
|
||||
commit
|
||||
|
||||
@@ -19,12 +19,14 @@ let branchPos = {};
|
||||
let commitPos = {};
|
||||
let lanes = [];
|
||||
let maxPos = 0;
|
||||
let dir = 'LR';
|
||||
const clear = () => {
|
||||
branchPos = {};
|
||||
commitPos = {};
|
||||
allCommitsDict = {};
|
||||
maxPos = 0;
|
||||
lanes = [];
|
||||
dir = 'LR';
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -77,6 +79,10 @@ const drawCommits = (svg, commits, modifyGraph) => {
|
||||
const gLabels = svg.append('g').attr('class', 'commit-labels');
|
||||
let pos = 0;
|
||||
|
||||
if (dir === 'TB') {
|
||||
pos = 30;
|
||||
}
|
||||
|
||||
const keys = Object.keys(commits);
|
||||
const sortedKeys = keys.sort((a, b) => {
|
||||
return commits[a].seq - commits[b].seq;
|
||||
@@ -84,8 +90,9 @@ const drawCommits = (svg, commits, modifyGraph) => {
|
||||
sortedKeys.forEach((key) => {
|
||||
const commit = commits[key];
|
||||
|
||||
const y = branchPos[commit.branch].pos;
|
||||
const x = pos + 10;
|
||||
const y = dir === 'TB' ? pos + 10 : branchPos[commit.branch].pos;
|
||||
const x = dir === 'TB' ? branchPos[commit.branch].pos : pos + 10;
|
||||
|
||||
// Don't draw the commits now but calculate the positioning which is used by the branch lines etc.
|
||||
if (modifyGraph) {
|
||||
let typeClass;
|
||||
@@ -208,7 +215,11 @@ const drawCommits = (svg, commits, modifyGraph) => {
|
||||
}
|
||||
}
|
||||
}
|
||||
commitPos[commit.id] = { x: pos + 10, y: y };
|
||||
if (dir === 'TB') {
|
||||
commitPos[commit.id] = { x: x, y: pos + 10 };
|
||||
} else {
|
||||
commitPos[commit.id] = { x: pos + 10, y: y };
|
||||
}
|
||||
|
||||
// The first iteration over the commits are for positioning purposes, this
|
||||
// is required for drawing the lines. The circles and labels is drawn after the labels
|
||||
@@ -240,14 +251,27 @@ const drawCommits = (svg, commits, modifyGraph) => {
|
||||
.attr('y', y + 13.5)
|
||||
.attr('width', bbox.width + 2 * py)
|
||||
.attr('height', bbox.height + 2 * py);
|
||||
text.attr('x', pos + 10 - bbox.width / 2);
|
||||
|
||||
if (dir === 'TB') {
|
||||
labelBkg.attr('x', x - (bbox.width + 4 * px + 5)).attr('y', y - 12);
|
||||
text.attr('x', x - (bbox.width + 4 * px)).attr('y', y + bbox.height - 12);
|
||||
}
|
||||
|
||||
if (dir !== 'TB') {
|
||||
text.attr('x', pos + 10 - bbox.width / 2);
|
||||
}
|
||||
if (gitGraphConfig.rotateCommitLabel) {
|
||||
let r_x = -7.5 - ((bbox.width + 10) / 25) * 9.5;
|
||||
let r_y = 10 + (bbox.width / 25) * 8.5;
|
||||
wrapper.attr(
|
||||
'transform',
|
||||
'translate(' + r_x + ', ' + r_y + ') rotate(' + -45 + ', ' + pos + ', ' + y + ')'
|
||||
);
|
||||
if (dir === 'TB') {
|
||||
text.attr('transform', 'rotate(' + -45 + ', ' + x + ', ' + y + ')');
|
||||
labelBkg.attr('transform', 'rotate(' + -45 + ', ' + x + ', ' + y + ')');
|
||||
} else {
|
||||
let r_x = -7.5 - ((bbox.width + 10) / 25) * 9.5;
|
||||
let r_y = 10 + (bbox.width / 25) * 8.5;
|
||||
wrapper.attr(
|
||||
'transform',
|
||||
'translate(' + r_x + ', ' + r_y + ') rotate(' + -45 + ', ' + pos + ', ' + y + ')'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (commit.tag) {
|
||||
@@ -280,6 +304,30 @@ const drawCommits = (svg, commits, modifyGraph) => {
|
||||
.attr('cy', ly)
|
||||
.attr('r', 1.5)
|
||||
.attr('class', 'tag-hole');
|
||||
|
||||
if (dir === 'TB') {
|
||||
rect
|
||||
.attr('class', 'tag-label-bkg')
|
||||
.attr(
|
||||
'points',
|
||||
`
|
||||
${x},${pos + py}
|
||||
${x},${pos - py}
|
||||
${x + 10},${pos - h2 - py}
|
||||
${x + 10 + tagBbox.width + px},${pos - h2 - py}
|
||||
${x + 10 + tagBbox.width + px},${pos + h2 + py}
|
||||
${x + 10},${pos + h2 + py}`
|
||||
)
|
||||
.attr('transform', 'translate(12,12) rotate(45, ' + x + ',' + pos + ')');
|
||||
hole
|
||||
.attr('cx', x + px / 2)
|
||||
.attr('cy', pos)
|
||||
.attr('transform', 'translate(12,12) rotate(45, ' + x + ',' + pos + ')');
|
||||
tag
|
||||
.attr('x', x + 5)
|
||||
.attr('y', pos + 3)
|
||||
.attr('transform', 'translate(14,14) rotate(45, ' + x + ',' + pos + ')');
|
||||
}
|
||||
}
|
||||
}
|
||||
pos += 50;
|
||||
@@ -365,46 +413,94 @@ const drawArrow = (svg, commit1, commit2, allCommits) => {
|
||||
colorClassNum = branchPos[commit2.branch].index;
|
||||
|
||||
const lineY = p1.y < p2.y ? findLane(p1.y, p2.y) : findLane(p2.y, p1.y);
|
||||
const lineX = p1.x < p2.x ? findLane(p1.x, p2.x) : findLane(p2.x, p1.x);
|
||||
|
||||
if (p1.y < p2.y) {
|
||||
lineDef = `M ${p1.x} ${p1.y} L ${p1.x} ${lineY - radius} ${arc} ${p1.x + offset} ${lineY} L ${
|
||||
p2.x - radius
|
||||
} ${lineY} ${arc2} ${p2.x} ${lineY + offset} L ${p2.x} ${p2.y}`;
|
||||
if (dir === 'TB') {
|
||||
if (p1.x < p2.x) {
|
||||
lineDef = `M ${p1.x} ${p1.y} L ${lineX - radius} ${p1.y} ${arc2} ${lineX} ${
|
||||
p1.y + offset
|
||||
} L ${lineX} ${p2.y - radius} ${arc} ${lineX + offset} ${p2.y} L ${p2.x} ${p2.y}`;
|
||||
} else {
|
||||
lineDef = `M ${p1.x} ${p1.y} L ${lineX + radius} ${p1.y} ${arc} ${lineX} ${
|
||||
p1.y + offset
|
||||
} L ${lineX} ${p2.y - radius} ${arc2} ${lineX - offset} ${p2.y} L ${p2.x} ${p2.y}`;
|
||||
}
|
||||
} else {
|
||||
lineDef = `M ${p1.x} ${p1.y} L ${p1.x} ${lineY + radius} ${arc2} ${
|
||||
p1.x + offset
|
||||
} ${lineY} L ${p2.x - radius} ${lineY} ${arc} ${p2.x} ${lineY - offset} L ${p2.x} ${p2.y}`;
|
||||
if (p1.y < p2.y) {
|
||||
lineDef = `M ${p1.x} ${p1.y} L ${p1.x} ${lineY - radius} ${arc} ${
|
||||
p1.x + offset
|
||||
} ${lineY} L ${p2.x - radius} ${lineY} ${arc2} ${p2.x} ${lineY + offset} L ${p2.x} ${p2.y}`;
|
||||
} else {
|
||||
lineDef = `M ${p1.x} ${p1.y} L ${p1.x} ${lineY + radius} ${arc2} ${
|
||||
p1.x + offset
|
||||
} ${lineY} L ${p2.x - radius} ${lineY} ${arc} ${p2.x} ${lineY - offset} L ${p2.x} ${p2.y}`;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (p1.y < p2.y) {
|
||||
arc = 'A 20 20, 0, 0, 0,';
|
||||
radius = 20;
|
||||
offset = 20;
|
||||
if (dir === 'TB') {
|
||||
if (p1.x < p2.x) {
|
||||
arc = 'A 20 20, 0, 0, 0,';
|
||||
arc2 = 'A 20 20, 0, 0, 1,';
|
||||
radius = 20;
|
||||
offset = 20;
|
||||
|
||||
// Figure out the color of the arrow,arrows going down take the color from the destination branch
|
||||
colorClassNum = branchPos[commit2.branch].index;
|
||||
// Figure out the color of the arrow,arrows going down take the color from the destination branch
|
||||
colorClassNum = branchPos[commit2.branch].index;
|
||||
|
||||
lineDef = `M ${p1.x} ${p1.y} L ${p1.x} ${p2.y - radius} ${arc} ${p1.x + offset} ${p2.y} L ${
|
||||
p2.x
|
||||
} ${p2.y}`;
|
||||
}
|
||||
if (p1.y > p2.y) {
|
||||
arc = 'A 20 20, 0, 0, 0,';
|
||||
radius = 20;
|
||||
offset = 20;
|
||||
lineDef = `M ${p1.x} ${p1.y} L ${p2.x - radius} ${p1.y} ${arc2} ${p2.x} ${
|
||||
p1.y + offset
|
||||
} L ${p2.x} ${p2.y}`;
|
||||
}
|
||||
if (p1.x > p2.x) {
|
||||
arc = 'A 20 20, 0, 0, 0,';
|
||||
arc2 = 'A 20 20, 0, 0, 1,';
|
||||
radius = 20;
|
||||
offset = 20;
|
||||
|
||||
// Arrows going up take the color from the source branch
|
||||
colorClassNum = branchPos[commit1.branch].index;
|
||||
lineDef = `M ${p1.x} ${p1.y} L ${p2.x - radius} ${p1.y} ${arc} ${p2.x} ${p1.y - offset} L ${
|
||||
p2.x
|
||||
} ${p2.y}`;
|
||||
}
|
||||
// Arrows going up take the color from the source branch
|
||||
colorClassNum = branchPos[commit1.branch].index;
|
||||
lineDef = `M ${p1.x} ${p1.y} L ${p1.x} ${p2.y - radius} ${arc2} ${p1.x - offset} ${
|
||||
p2.y
|
||||
} L ${p2.x} ${p2.y}`;
|
||||
}
|
||||
|
||||
if (p1.y === p2.y) {
|
||||
colorClassNum = branchPos[commit1.branch].index;
|
||||
lineDef = `M ${p1.x} ${p1.y} L ${p1.x} ${p2.y - radius} ${arc} ${p1.x + offset} ${p2.y} L ${
|
||||
p2.x
|
||||
} ${p2.y}`;
|
||||
if (p1.x === p2.x) {
|
||||
colorClassNum = branchPos[commit1.branch].index;
|
||||
lineDef = `M ${p1.x} ${p1.y} L ${p1.x + radius} ${p1.y} ${arc} ${p1.x + offset} ${
|
||||
p2.y + radius
|
||||
} L ${p2.x} ${p2.y}`;
|
||||
}
|
||||
} else {
|
||||
if (p1.y < p2.y) {
|
||||
arc = 'A 20 20, 0, 0, 0,';
|
||||
radius = 20;
|
||||
offset = 20;
|
||||
|
||||
// Figure out the color of the arrow,arrows going down take the color from the destination branch
|
||||
colorClassNum = branchPos[commit2.branch].index;
|
||||
|
||||
lineDef = `M ${p1.x} ${p1.y} L ${p1.x} ${p2.y - radius} ${arc} ${p1.x + offset} ${p2.y} L ${
|
||||
p2.x
|
||||
} ${p2.y}`;
|
||||
}
|
||||
if (p1.y > p2.y) {
|
||||
arc = 'A 20 20, 0, 0, 0,';
|
||||
radius = 20;
|
||||
offset = 20;
|
||||
|
||||
// Arrows going up take the color from the source branch
|
||||
colorClassNum = branchPos[commit1.branch].index;
|
||||
lineDef = `M ${p1.x} ${p1.y} L ${p2.x - radius} ${p1.y} ${arc} ${p2.x} ${p1.y - offset} L ${
|
||||
p2.x
|
||||
} ${p2.y}`;
|
||||
}
|
||||
|
||||
if (p1.y === p2.y) {
|
||||
colorClassNum = branchPos[commit1.branch].index;
|
||||
lineDef = `M ${p1.x} ${p1.y} L ${p1.x} ${p2.y - radius} ${arc} ${p1.x + offset} ${p2.y} L ${
|
||||
p2.x
|
||||
} ${p2.y}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
svg
|
||||
@@ -445,6 +541,13 @@ const drawBranches = (svg, branches) => {
|
||||
line.attr('y2', pos);
|
||||
line.attr('class', 'branch branch' + adjustIndexForTheme);
|
||||
|
||||
if (dir === 'TB') {
|
||||
line.attr('y1', 30);
|
||||
line.attr('x1', pos);
|
||||
line.attr('y2', maxPos);
|
||||
line.attr('x2', pos);
|
||||
}
|
||||
|
||||
lanes.push(pos);
|
||||
|
||||
let name = branch.name;
|
||||
@@ -467,7 +570,6 @@ const drawBranches = (svg, branches) => {
|
||||
.attr('y', -bbox.height / 2 + 8)
|
||||
.attr('width', bbox.width + 18)
|
||||
.attr('height', bbox.height + 4);
|
||||
|
||||
label.attr(
|
||||
'transform',
|
||||
'translate(' +
|
||||
@@ -476,7 +578,13 @@ const drawBranches = (svg, branches) => {
|
||||
(pos - bbox.height / 2 - 1) +
|
||||
')'
|
||||
);
|
||||
bkg.attr('transform', 'translate(' + -19 + ', ' + (pos - bbox.height / 2) + ')');
|
||||
if (dir === 'TB') {
|
||||
bkg.attr('x', pos - bbox.width / 2 - 10).attr('y', 0);
|
||||
label.attr('transform', 'translate(' + (pos - bbox.width / 2 - 5) + ', ' + 0 + ')');
|
||||
}
|
||||
if (dir !== 'TB') {
|
||||
bkg.attr('transform', 'translate(' + -19 + ', ' + (pos - bbox.height / 2) + ')');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -495,15 +603,24 @@ export const draw = function (txt, id, ver, diagObj) {
|
||||
|
||||
allCommitsDict = diagObj.db.getCommits();
|
||||
const branches = diagObj.db.getBranchesAsObjArray();
|
||||
|
||||
// Position branches vertically
|
||||
dir = diagObj.db.getDirection();
|
||||
const diagram = select(`[id="${id}"]`);
|
||||
// Position branches
|
||||
let pos = 0;
|
||||
branches.forEach((branch, index) => {
|
||||
branchPos[branch.name] = { pos, index };
|
||||
pos += 50 + (gitGraphConfig.rotateCommitLabel ? 40 : 0);
|
||||
});
|
||||
const labelElement = drawText(branch.name);
|
||||
const g = diagram.append('g');
|
||||
const branchLabel = g.insert('g').attr('class', 'branchLabel');
|
||||
const label = branchLabel.insert('g').attr('class', 'label branch-label');
|
||||
label.node().appendChild(labelElement);
|
||||
let bbox = labelElement.getBBox();
|
||||
|
||||
const diagram = select(`[id="${id}"]`);
|
||||
branchPos[branch.name] = { pos, index };
|
||||
pos += 50 + (gitGraphConfig.rotateCommitLabel ? 40 : 0) + (dir === 'TB' ? bbox.width / 2 : 0);
|
||||
label.remove();
|
||||
branchLabel.remove();
|
||||
g.remove();
|
||||
});
|
||||
|
||||
drawCommits(diagram, allCommitsDict, false);
|
||||
if (gitGraphConfig.showBranches) {
|
||||
|
||||
@@ -9,10 +9,6 @@
|
||||
|
||||
%x string
|
||||
%x options
|
||||
%x open_directive
|
||||
%x type_directive
|
||||
%x arg_directive
|
||||
%x close_directive
|
||||
%x acc_title
|
||||
%x acc_descr
|
||||
%x acc_descr_multiline
|
||||
@@ -20,11 +16,6 @@
|
||||
|
||||
|
||||
%%
|
||||
\%\%\{ { this.begin('open_directive'); return 'open_directive'; }
|
||||
<open_directive>((?:(?!\}\%\%)[^:.])*) { this.begin('type_directive'); return 'type_directive'; }
|
||||
<type_directive>":" { this.popState(); this.begin('arg_directive'); return ':'; }
|
||||
<type_directive,arg_directive>\}\%\% { this.popState(); this.popState(); return 'close_directive'; }
|
||||
<arg_directive>((?:(?!\}\%\%).|\n)*) return 'arg_directive';
|
||||
accTitle\s*":"\s* { this.begin("acc_title");return 'acc_title'; }
|
||||
<acc_title>(?!\n|;|#)*[^\n]* { this.popState(); return "acc_title_value"; }
|
||||
accDescr\s*":"\s* { this.begin("acc_descr");return 'acc_descr'; }
|
||||
@@ -51,7 +42,7 @@ cherry\-pick(?=\s|$) return 'CHERRY_PICK';
|
||||
// "reset" return 'RESET';
|
||||
checkout(?=\s|$) return 'CHECKOUT';
|
||||
"LR" return 'DIR';
|
||||
"BT" return 'DIR';
|
||||
"TB" return 'DIR';
|
||||
":" return ':';
|
||||
"^" return 'CARET'
|
||||
"options"\r?\n this.begin("options"); //
|
||||
@@ -76,7 +67,6 @@ checkout(?=\s|$) return 'CHECKOUT';
|
||||
|
||||
start
|
||||
: eol start
|
||||
| directive start
|
||||
| GG document EOF{ return $3; }
|
||||
| GG ':' document EOF{ return $3; }
|
||||
| GG DIR ':' document EOF {yy.setDirection($2); return $4;}
|
||||
@@ -240,27 +230,6 @@ commitType
|
||||
| HIGHLIGHT { $$=yy.commitType.HIGHLIGHT;}
|
||||
;
|
||||
|
||||
directive
|
||||
: openDirective typeDirective closeDirective
|
||||
| openDirective typeDirective ':' argDirective closeDirective
|
||||
;
|
||||
|
||||
openDirective
|
||||
: open_directive { yy.parseDirective('%%{', 'open_directive'); }
|
||||
;
|
||||
|
||||
typeDirective
|
||||
: type_directive { yy.parseDirective($1, 'type_directive'); }
|
||||
;
|
||||
|
||||
argDirective
|
||||
: arg_directive { $1 = $1.trim().replace(/'/g, '"'); yy.parseDirective($1, 'arg_directive'); }
|
||||
;
|
||||
|
||||
closeDirective
|
||||
: close_directive { yy.parseDirective('}%%', 'close_directive', 'gitGraph'); }
|
||||
;
|
||||
|
||||
ref
|
||||
: ID
|
||||
| STR
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { select } from 'd3';
|
||||
import { log } from '../../logger.js';
|
||||
import { getConfig } from '../../config.js';
|
||||
import type { DrawDefinition, HTML, SVG } from '../../diagram-api/types.js';
|
||||
import { configureSvgSize } from '../../setupGraphViewbox.js';
|
||||
import type { DrawDefinition, Group, SVG } from '../../diagram-api/types.js';
|
||||
import { selectSvgElement } from '../../rendering-util/selectSvgElement.js';
|
||||
|
||||
/**
|
||||
* Draws a an info picture in the tag with id: id based on the graph definition in text.
|
||||
@@ -11,40 +11,20 @@ import type { DrawDefinition, HTML, SVG } from '../../diagram-api/types.js';
|
||||
* @param version - MermaidJS version.
|
||||
*/
|
||||
const draw: DrawDefinition = (text, id, version) => {
|
||||
try {
|
||||
log.debug('rendering info diagram\n' + text);
|
||||
log.debug('rendering info diagram\n' + text);
|
||||
|
||||
const { securityLevel } = getConfig();
|
||||
// handle root and document for when rendering in sandbox mode
|
||||
let sandboxElement: HTML | undefined;
|
||||
let document: Document | null | undefined;
|
||||
if (securityLevel === 'sandbox') {
|
||||
sandboxElement = select('#i' + id);
|
||||
document = sandboxElement.nodes()[0].contentDocument;
|
||||
}
|
||||
const svg: SVG = selectSvgElement(id);
|
||||
configureSvgSize(svg, 100, 400, true);
|
||||
|
||||
// @ts-ignore - figure out how to assign HTML to document type
|
||||
const root: HTML =
|
||||
sandboxElement !== undefined && document !== undefined && document !== null
|
||||
? select(document)
|
||||
: select('body');
|
||||
|
||||
const svg: SVG = root.select('#' + id);
|
||||
svg.attr('height', 100);
|
||||
svg.attr('width', 400);
|
||||
|
||||
const g = svg.append('g');
|
||||
|
||||
g.append('text') // text label for the x axis
|
||||
.attr('x', 100)
|
||||
.attr('y', 40)
|
||||
.attr('class', 'version')
|
||||
.attr('font-size', '32px')
|
||||
.style('text-anchor', 'middle')
|
||||
.text('v ' + version);
|
||||
} catch (e) {
|
||||
log.error('error while rendering info diagram', e);
|
||||
}
|
||||
const group: Group = svg.append('g');
|
||||
group
|
||||
.append('text')
|
||||
.attr('x', 100)
|
||||
.attr('y', 40)
|
||||
.attr('class', 'version')
|
||||
.attr('font-size', 32)
|
||||
.style('text-anchor', 'middle')
|
||||
.text(`v${version}`);
|
||||
};
|
||||
|
||||
export const renderer = { draw };
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @ts-ignore: TODO Fix ts errors
|
||||
// @ts-ignore: JISON doesn't support types
|
||||
import mindmapParser from './parser/mindmap.jison';
|
||||
import * as mindmapDb from './mindmapDb.js';
|
||||
import mindmapRenderer from './mindmapRenderer.js';
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
"[" { this.begin('NODE');return 'NODE_DSTART'; }
|
||||
[\s]+ return 'SPACELIST' /* skip all whitespace */ ;
|
||||
// !(-\() return 'NODE_ID';
|
||||
[^\(\[\n\-\)\{\}]+ return 'NODE_ID';
|
||||
[^\(\[\n\)\{\}]+ return 'NODE_ID';
|
||||
<<EOF>> return 'EOF';
|
||||
<NODE>["][`] { this.begin("NSTR2");}
|
||||
<NSTR2>[^`"]+ { return "NODE_DESCR";}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
name,amounts
|
||||
Foo, 33
|
||||
Rishab, 12
|
||||
Alexis, 41
|
||||
Tom, 16
|
||||
Courtney, 59
|
||||
Christina, 38
|
||||
Jack, 21
|
||||
Mickey, 25
|
||||
Paul, 30
|
||||
|
@@ -8,19 +8,10 @@
|
||||
|
||||
%x string
|
||||
%x title
|
||||
%x open_directive
|
||||
%x type_directive
|
||||
%x arg_directive
|
||||
%x close_directive
|
||||
%x acc_title
|
||||
%x acc_descr
|
||||
%x acc_descr_multiline
|
||||
%%
|
||||
\%\%\{ { this.begin('open_directive'); return 'open_directive'; }
|
||||
<open_directive>((?:(?!\}\%\%)[^:.])*) { this.begin('type_directive'); return 'type_directive'; }
|
||||
<type_directive>":" { this.popState(); this.begin('arg_directive'); return ':'; }
|
||||
<type_directive,arg_directive>\}\%\% { this.popState(); this.popState(); return 'close_directive'; }
|
||||
<arg_directive>((?:(?!\}\%\%).|\n)*) return 'arg_directive';
|
||||
\%\%(?!\{)[^\n]* /* skip comments */
|
||||
[^\}]\%\%[^\n]* /* skip comments */{ /*console.log('');*/ }
|
||||
[\n\r]+ return 'NEWLINE';
|
||||
@@ -52,7 +43,6 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multili
|
||||
|
||||
start
|
||||
: eol start
|
||||
| directive start
|
||||
| PIE document
|
||||
| PIE showData document {yy.setShowData(true);}
|
||||
;
|
||||
@@ -73,34 +63,12 @@ statement
|
||||
| acc_title acc_title_value { $$=$2.trim();yy.setAccTitle($$); }
|
||||
| acc_descr acc_descr_value { $$=$2.trim();yy.setAccDescription($$); }
|
||||
| acc_descr_multiline_value { $$=$1.trim();yy.setAccDescription($$); } | section {yy.addSection($1.substr(8));$$=$1.substr(8);}
|
||||
| directive
|
||||
;
|
||||
|
||||
directive
|
||||
: openDirective typeDirective closeDirective
|
||||
| openDirective typeDirective ':' argDirective closeDirective
|
||||
;
|
||||
|
||||
eol
|
||||
: NEWLINE
|
||||
| ';'
|
||||
| EOF
|
||||
;
|
||||
|
||||
openDirective
|
||||
: open_directive { yy.parseDirective('%%{', 'open_directive'); }
|
||||
;
|
||||
|
||||
typeDirective
|
||||
: type_directive { yy.parseDirective($1, 'type_directive'); }
|
||||
;
|
||||
|
||||
argDirective
|
||||
: arg_directive { $1 = $1.trim().replace(/'/g, '"'); yy.parseDirective($1, 'arg_directive'); }
|
||||
;
|
||||
|
||||
closeDirective
|
||||
: close_directive { yy.parseDirective('}%%', 'close_directive', 'pie'); }
|
||||
;
|
||||
|
||||
%%
|
||||
|
||||
@@ -1,132 +0,0 @@
|
||||
import pieDb from '../pieDb.js';
|
||||
import pie from './pie.jison';
|
||||
import { setConfig } from '../../../config.js';
|
||||
|
||||
setConfig({
|
||||
securityLevel: 'strict',
|
||||
});
|
||||
|
||||
describe('when parsing pie', function () {
|
||||
beforeEach(function () {
|
||||
pie.parser.yy = pieDb;
|
||||
pie.parser.yy.clear();
|
||||
});
|
||||
it('should handle very simple pie', function () {
|
||||
const res = pie.parser.parse(`pie
|
||||
"ash" : 100
|
||||
`);
|
||||
const sections = pieDb.getSections();
|
||||
const section1 = sections['ash'];
|
||||
expect(section1).toBe(100);
|
||||
});
|
||||
it('should handle simple pie', function () {
|
||||
const res = pie.parser.parse(`pie
|
||||
"ash" : 60
|
||||
"bat" : 40
|
||||
`);
|
||||
const sections = pieDb.getSections();
|
||||
const section1 = sections['ash'];
|
||||
expect(section1).toBe(60);
|
||||
});
|
||||
it('should handle simple pie with comments', function () {
|
||||
const res = pie.parser.parse(`pie
|
||||
%% comments
|
||||
"ash" : 60
|
||||
"bat" : 40
|
||||
`);
|
||||
const sections = pieDb.getSections();
|
||||
const section1 = sections['ash'];
|
||||
expect(section1).toBe(60);
|
||||
});
|
||||
|
||||
it('should handle simple pie with a directive', function () {
|
||||
const res = pie.parser.parse(`%%{init: {'logLevel':0}}%%
|
||||
pie
|
||||
"ash" : 60
|
||||
"bat" : 40
|
||||
`);
|
||||
const sections = pieDb.getSections();
|
||||
const section1 = sections['ash'];
|
||||
expect(section1).toBe(60);
|
||||
});
|
||||
|
||||
it('should handle simple pie with a title', function () {
|
||||
const res = pie.parser.parse(`pie title a 60/40 pie
|
||||
"ash" : 60
|
||||
"bat" : 40
|
||||
`);
|
||||
const sections = pieDb.getSections();
|
||||
const title = pieDb.getDiagramTitle();
|
||||
const section1 = sections['ash'];
|
||||
expect(section1).toBe(60);
|
||||
expect(title).toBe('a 60/40 pie');
|
||||
});
|
||||
|
||||
it('should handle simple pie without an acc description (accDescr)', function () {
|
||||
const res = pie.parser.parse(`pie title a neat chart
|
||||
"ash" : 60
|
||||
"bat" : 40
|
||||
`);
|
||||
|
||||
const sections = pieDb.getSections();
|
||||
const title = pieDb.getDiagramTitle();
|
||||
const description = pieDb.getAccDescription();
|
||||
const section1 = sections['ash'];
|
||||
expect(section1).toBe(60);
|
||||
expect(title).toBe('a neat chart');
|
||||
expect(description).toBe('');
|
||||
});
|
||||
|
||||
it('should handle simple pie with an acc description (accDescr)', function () {
|
||||
const res = pie.parser.parse(`pie title a neat chart
|
||||
accDescr: a neat description
|
||||
"ash" : 60
|
||||
"bat" : 40
|
||||
`);
|
||||
|
||||
const sections = pieDb.getSections();
|
||||
const title = pieDb.getDiagramTitle();
|
||||
const description = pieDb.getAccDescription();
|
||||
const section1 = sections['ash'];
|
||||
expect(section1).toBe(60);
|
||||
expect(title).toBe('a neat chart');
|
||||
expect(description).toBe('a neat description');
|
||||
});
|
||||
it('should handle simple pie with a multiline acc description (accDescr)', function () {
|
||||
const res = pie.parser.parse(`pie title a neat chart
|
||||
accDescr {
|
||||
a neat description
|
||||
on multiple lines
|
||||
}
|
||||
"ash" : 60
|
||||
"bat" : 40
|
||||
`);
|
||||
|
||||
const sections = pieDb.getSections();
|
||||
const title = pieDb.getDiagramTitle();
|
||||
const description = pieDb.getAccDescription();
|
||||
const section1 = sections['ash'];
|
||||
expect(section1).toBe(60);
|
||||
expect(title).toBe('a neat chart');
|
||||
expect(description).toBe('a neat description\non multiple lines');
|
||||
});
|
||||
|
||||
it('should handle simple pie with positive decimal', function () {
|
||||
const res = pie.parser.parse(`pie
|
||||
"ash" : 60.67
|
||||
"bat" : 40
|
||||
`);
|
||||
const sections = pieDb.getSections();
|
||||
const section1 = sections['ash'];
|
||||
expect(section1).toBe(60.67);
|
||||
});
|
||||
|
||||
it('should handle simple pie with negative decimal', function () {
|
||||
expect(() => {
|
||||
pie.parser.parse(`pie
|
||||
"ash" : 60.67
|
||||
"bat" : 40..12
|
||||
`);
|
||||
}).toThrowError();
|
||||
});
|
||||
});
|
||||
169
packages/mermaid/src/diagrams/pie/pie.spec.ts
Normal file
169
packages/mermaid/src/diagrams/pie/pie.spec.ts
Normal file
@@ -0,0 +1,169 @@
|
||||
// @ts-ignore: JISON doesn't support types
|
||||
import { parser } from './parser/pie.jison';
|
||||
import { DEFAULT_PIE_DB, db } from './pieDb.js';
|
||||
import { setConfig } from '../../config.js';
|
||||
|
||||
setConfig({
|
||||
securityLevel: 'strict',
|
||||
});
|
||||
|
||||
describe('pie', () => {
|
||||
beforeAll(() => {
|
||||
parser.yy = db;
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
parser.yy.clear();
|
||||
});
|
||||
|
||||
describe('parse', () => {
|
||||
it('should handle very simple pie', () => {
|
||||
parser.parse(`pie
|
||||
"ash": 100
|
||||
`);
|
||||
|
||||
const sections = db.getSections();
|
||||
expect(sections['ash']).toBe(100);
|
||||
});
|
||||
|
||||
it('should handle simple pie', () => {
|
||||
parser.parse(`pie
|
||||
"ash" : 60
|
||||
"bat" : 40
|
||||
`);
|
||||
|
||||
const sections = db.getSections();
|
||||
expect(sections['ash']).toBe(60);
|
||||
expect(sections['bat']).toBe(40);
|
||||
});
|
||||
|
||||
it('should handle simple pie with showData', () => {
|
||||
parser.parse(`pie showData
|
||||
"ash" : 60
|
||||
"bat" : 40
|
||||
`);
|
||||
|
||||
expect(db.getShowData()).toBeTruthy();
|
||||
|
||||
const sections = db.getSections();
|
||||
expect(sections['ash']).toBe(60);
|
||||
expect(sections['bat']).toBe(40);
|
||||
});
|
||||
|
||||
it('should handle simple pie with comments', () => {
|
||||
parser.parse(`pie
|
||||
%% comments
|
||||
"ash" : 60
|
||||
"bat" : 40
|
||||
`);
|
||||
|
||||
const sections = db.getSections();
|
||||
expect(sections['ash']).toBe(60);
|
||||
expect(sections['bat']).toBe(40);
|
||||
});
|
||||
|
||||
it('should handle simple pie with a title', () => {
|
||||
parser.parse(`pie title a 60/40 pie
|
||||
"ash" : 60
|
||||
"bat" : 40
|
||||
`);
|
||||
|
||||
expect(db.getDiagramTitle()).toBe('a 60/40 pie');
|
||||
|
||||
const sections = db.getSections();
|
||||
expect(sections['ash']).toBe(60);
|
||||
expect(sections['bat']).toBe(40);
|
||||
});
|
||||
|
||||
it('should handle simple pie with an acc title (accTitle)', () => {
|
||||
parser.parse(`pie title a neat chart
|
||||
accTitle: a neat acc title
|
||||
"ash" : 60
|
||||
"bat" : 40
|
||||
`);
|
||||
|
||||
expect(db.getDiagramTitle()).toBe('a neat chart');
|
||||
|
||||
expect(db.getAccTitle()).toBe('a neat acc title');
|
||||
|
||||
const sections = db.getSections();
|
||||
expect(sections['ash']).toBe(60);
|
||||
expect(sections['bat']).toBe(40);
|
||||
});
|
||||
|
||||
it('should handle simple pie with an acc description (accDescr)', () => {
|
||||
parser.parse(`pie title a neat chart
|
||||
accDescr: a neat description
|
||||
"ash" : 60
|
||||
"bat" : 40
|
||||
`);
|
||||
|
||||
expect(db.getDiagramTitle()).toBe('a neat chart');
|
||||
|
||||
expect(db.getAccDescription()).toBe('a neat description');
|
||||
|
||||
const sections = db.getSections();
|
||||
expect(sections['ash']).toBe(60);
|
||||
expect(sections['bat']).toBe(40);
|
||||
});
|
||||
|
||||
it('should handle simple pie with a multiline acc description (accDescr)', () => {
|
||||
parser.parse(`pie title a neat chart
|
||||
accDescr {
|
||||
a neat description
|
||||
on multiple lines
|
||||
}
|
||||
"ash" : 60
|
||||
"bat" : 40
|
||||
`);
|
||||
|
||||
expect(db.getDiagramTitle()).toBe('a neat chart');
|
||||
|
||||
expect(db.getAccDescription()).toBe('a neat description\non multiple lines');
|
||||
|
||||
const sections = db.getSections();
|
||||
expect(sections['ash']).toBe(60);
|
||||
expect(sections['bat']).toBe(40);
|
||||
});
|
||||
|
||||
it('should handle simple pie with positive decimal', () => {
|
||||
parser.parse(`pie
|
||||
"ash" : 60.67
|
||||
"bat" : 40
|
||||
`);
|
||||
|
||||
const sections = db.getSections();
|
||||
expect(sections['ash']).toBe(60.67);
|
||||
expect(sections['bat']).toBe(40);
|
||||
});
|
||||
|
||||
it('should handle simple pie with negative decimal', () => {
|
||||
expect(() => {
|
||||
parser.parse(`pie
|
||||
"ash" : -60.67
|
||||
"bat" : 40.12
|
||||
`);
|
||||
}).toThrowError();
|
||||
});
|
||||
});
|
||||
|
||||
describe('config', () => {
|
||||
it.todo('setConfig', () => {
|
||||
// db.setConfig({ useWidth: 850, useMaxWidth: undefined });
|
||||
|
||||
const config = db.getConfig();
|
||||
expect(config.useWidth).toBe(850);
|
||||
expect(config.useMaxWidth).toBeTruthy();
|
||||
});
|
||||
|
||||
it('getConfig', () => {
|
||||
expect(db.getConfig()).toStrictEqual(DEFAULT_PIE_DB.config);
|
||||
});
|
||||
|
||||
it.todo('resetConfig', () => {
|
||||
// db.setConfig({ textPosition: 0 });
|
||||
// db.resetConfig();
|
||||
expect(db.getConfig().textPosition).toStrictEqual(DEFAULT_PIE_DB.config.textPosition);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,69 +0,0 @@
|
||||
import { log } from '../../logger.js';
|
||||
import mermaidAPI from '../../mermaidAPI.js';
|
||||
import * as configApi from '../../config.js';
|
||||
import common from '../common/common.js';
|
||||
import {
|
||||
setAccTitle,
|
||||
getAccTitle,
|
||||
setDiagramTitle,
|
||||
getDiagramTitle,
|
||||
getAccDescription,
|
||||
setAccDescription,
|
||||
clear as commonClear,
|
||||
} from '../../commonDb.js';
|
||||
|
||||
let sections = {};
|
||||
let showData = false;
|
||||
|
||||
export const parseDirective = function (statement, context, type) {
|
||||
mermaidAPI.parseDirective(this, statement, context, type);
|
||||
};
|
||||
|
||||
const addSection = function (id, value) {
|
||||
id = common.sanitizeText(id, configApi.getConfig());
|
||||
if (sections[id] === undefined) {
|
||||
sections[id] = value;
|
||||
log.debug('Added new section :', id);
|
||||
}
|
||||
};
|
||||
const getSections = () => sections;
|
||||
|
||||
const setShowData = function (toggle) {
|
||||
showData = toggle;
|
||||
};
|
||||
|
||||
const getShowData = function () {
|
||||
return showData;
|
||||
};
|
||||
|
||||
const cleanupValue = function (value) {
|
||||
if (value.substring(0, 1) === ':') {
|
||||
value = value.substring(1).trim();
|
||||
return Number(value.trim());
|
||||
} else {
|
||||
return Number(value.trim());
|
||||
}
|
||||
};
|
||||
|
||||
const clear = function () {
|
||||
sections = {};
|
||||
showData = false;
|
||||
commonClear();
|
||||
};
|
||||
|
||||
export default {
|
||||
parseDirective,
|
||||
getConfig: () => configApi.getConfig().pie,
|
||||
addSection,
|
||||
getSections,
|
||||
cleanupValue,
|
||||
clear,
|
||||
setAccTitle,
|
||||
getAccTitle,
|
||||
setDiagramTitle,
|
||||
getDiagramTitle,
|
||||
setShowData,
|
||||
getShowData,
|
||||
getAccDescription,
|
||||
setAccDescription,
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user