mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-11-19 20:24:16 +01:00
Merge branch 'develop' into pr/rhysd/4359
* develop: (453 commits) chore: Minor fixes chore: Build docs Use develop as base on develop branch. Update renovate json update link update announcement and blog pages Remove `--force` flag Tweak editor.bash update link chore: update browsers list Update integrations-community: add Drupal and module. Support Firefox Address review comments Change run symbol feat: Make the examples interactive in the documentation site. Add langium chore: update browsers list chore(deps): update all patch dependencies chore(deps): update all minor dependencies Update keywords and description ...
This commit is contained in:
@@ -39,15 +39,10 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@braintree/sanitize-url": "^6.0.1",
|
||||
"cytoscape": "^3.23.0",
|
||||
"cytoscape-cose-bilkent": "^4.1.0",
|
||||
"cytoscape-fcose": "^2.1.0",
|
||||
"d3": "^7.0.0",
|
||||
"khroma": "^2.0.0",
|
||||
"non-layered-tidy-tree-layout": "^2.0.2"
|
||||
"khroma": "^2.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cytoscape": "^3.19.9",
|
||||
"concurrently": "^8.0.0",
|
||||
"rimraf": "^5.0.0",
|
||||
"mermaid": "workspace:*"
|
||||
|
||||
45
packages/mermaid-flowchart-elk/package.json
Normal file
45
packages/mermaid-flowchart-elk/package.json
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "@mermaid-js/flowchart-elk",
|
||||
"version": "1.0.0-rc.1",
|
||||
"description": "Flowchart plugin for mermaid with ELK layout",
|
||||
"module": "dist/mermaid-flowchart-elk.core.mjs",
|
||||
"types": "dist/packages/mermaid-flowchart-elk/src/detector.d.ts",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/mermaid-flowchart-elk.core.mjs",
|
||||
"types": "./dist/packages/mermaid-flowchart-elk/src/detector.d.ts"
|
||||
},
|
||||
"./*": "./*"
|
||||
},
|
||||
"keywords": [
|
||||
"diagram",
|
||||
"markdown",
|
||||
"flowchart",
|
||||
"elk",
|
||||
"mermaid"
|
||||
],
|
||||
"scripts": {
|
||||
"prepublishOnly": "pnpm -w run build"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/mermaid-js/mermaid"
|
||||
},
|
||||
"author": "Knut Sveidqvist",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"d3": "^7.4.0",
|
||||
"dagre-d3-es": "7.0.10",
|
||||
"khroma": "^2.0.0",
|
||||
"elkjs": "^0.9.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"concurrently": "^8.0.0",
|
||||
"rimraf": "^5.0.0",
|
||||
"mermaid": "workspace:^"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
32
packages/mermaid-flowchart-elk/src/detector.ts
Normal file
32
packages/mermaid-flowchart-elk/src/detector.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import type {
|
||||
ExternalDiagramDefinition,
|
||||
DiagramDetector,
|
||||
DiagramLoader,
|
||||
} from '../../mermaid/src/diagram-api/types.js';
|
||||
|
||||
const id = 'flowchart-elk';
|
||||
|
||||
const detector: DiagramDetector = (txt, config): boolean => {
|
||||
if (
|
||||
// If diagram explicitly states flowchart-elk
|
||||
/^\s*flowchart-elk/.test(txt) ||
|
||||
// If a flowchart/graph diagram has their default renderer set to elk
|
||||
(/^\s*flowchart|graph/.test(txt) && config?.flowchart?.defaultRenderer === 'elk')
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const loader: DiagramLoader = async () => {
|
||||
const { diagram } = await import('./diagram-definition.js');
|
||||
return { id, diagram };
|
||||
};
|
||||
|
||||
const plugin: ExternalDiagramDefinition = {
|
||||
id,
|
||||
detector,
|
||||
loader,
|
||||
};
|
||||
|
||||
export default plugin;
|
||||
12
packages/mermaid-flowchart-elk/src/diagram-definition.ts
Normal file
12
packages/mermaid-flowchart-elk/src/diagram-definition.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
// @ts-ignore: JISON typing missing
|
||||
import parser from '../../mermaid/src/diagrams/flowchart/parser/flow.jison';
|
||||
import * as db from '../../mermaid/src/diagrams/flowchart/flowDb.js';
|
||||
import styles from '../../mermaid/src/diagrams/flowchart/styles.js';
|
||||
import renderer from './flowRenderer-elk.js';
|
||||
|
||||
export const diagram = {
|
||||
db,
|
||||
renderer,
|
||||
parser,
|
||||
styles,
|
||||
};
|
||||
@@ -1,17 +1,17 @@
|
||||
import { select, line, curveLinear } from 'd3';
|
||||
import { insertNode } from '../../../dagre-wrapper/nodes.js';
|
||||
import insertMarkers from '../../../dagre-wrapper/markers.js';
|
||||
import { insertEdgeLabel } from '../../../dagre-wrapper/edges.js';
|
||||
import { insertNode } from '../../mermaid/src/dagre-wrapper/nodes.js';
|
||||
import insertMarkers from '../../mermaid/src/dagre-wrapper/markers.js';
|
||||
import { insertEdgeLabel } from '../../mermaid/src/dagre-wrapper/edges.js';
|
||||
import { findCommonAncestor } from './render-utils.js';
|
||||
import { labelHelper } from '../../../dagre-wrapper/shapes/util.js';
|
||||
import { getConfig } from '../../../config.js';
|
||||
import { log } from '../../../logger.js';
|
||||
import { setupGraphViewbox } from '../../../setupGraphViewbox.js';
|
||||
import common from '../../common/common.js';
|
||||
import { interpolateToCurve, getStylesFromArray } from '../../../utils.js';
|
||||
import { labelHelper } from '../../mermaid/src/dagre-wrapper/shapes/util.js';
|
||||
import { getConfig } from '../../mermaid/src/config.js';
|
||||
import { log } from '../../mermaid/src/logger.js';
|
||||
import { setupGraphViewbox } from '../../mermaid/src/setupGraphViewbox.js';
|
||||
import common from '../../mermaid/src/diagrams/common/common.js';
|
||||
import { interpolateToCurve, getStylesFromArray } from '../../mermaid/src/utils.js';
|
||||
import ELK from 'elkjs/lib/elk.bundled.js';
|
||||
import { getLineFunctionsWithOffset } from '../../../utils/lineWithOffset.js';
|
||||
import { addEdgeMarkers } from '../../../dagre-wrapper/edgeMarker.js';
|
||||
import { getLineFunctionsWithOffset } from '../../mermaid/src/utils/lineWithOffset.js';
|
||||
import { addEdgeMarkers } from '../../mermaid/src/dagre-wrapper/edgeMarker.js';
|
||||
|
||||
const elk = new ELK();
|
||||
|
||||
@@ -93,13 +93,13 @@ export const addVertices = async function (vert, svgId, root, doc, diagObj, pare
|
||||
},
|
||||
];
|
||||
|
||||
let radious = 0;
|
||||
let radius = 0;
|
||||
let _shape = '';
|
||||
let layoutOptions = {};
|
||||
// Set the shape based parameters
|
||||
switch (vertex.type) {
|
||||
case 'round':
|
||||
radious = 5;
|
||||
radius = 5;
|
||||
_shape = 'rect';
|
||||
break;
|
||||
case 'square':
|
||||
@@ -163,8 +163,8 @@ export const addVertices = async function (vert, svgId, root, doc, diagObj, pare
|
||||
shape: _shape,
|
||||
labelText: vertexText,
|
||||
labelType: vertex.labelType,
|
||||
rx: radious,
|
||||
ry: radious,
|
||||
rx: radius,
|
||||
ry: radius,
|
||||
class: classStr,
|
||||
style: styles.style,
|
||||
id: vertex.id,
|
||||
@@ -309,13 +309,12 @@ const getNextPosition = (position, edgeDirection, graphDirection) => {
|
||||
},
|
||||
};
|
||||
portPos.TD = portPos.TB;
|
||||
log.info('abc88', graphDirection, edgeDirection, position);
|
||||
return portPos[graphDirection][edgeDirection][position];
|
||||
// return 'south';
|
||||
};
|
||||
|
||||
const getNextPort = (node, edgeDirection, graphDirection) => {
|
||||
log.info('getNextPort abc88', { node, edgeDirection, graphDirection });
|
||||
log.info('getNextPort', { node, edgeDirection, graphDirection });
|
||||
if (!portPos[node]) {
|
||||
switch (graphDirection) {
|
||||
case 'TB':
|
||||
@@ -595,7 +594,7 @@ const addMarkersToEdge = function (svgPath, edgeData, diagramType, arrowMarkerAb
|
||||
*
|
||||
* @param text
|
||||
* @param diagObj
|
||||
* @returns {Record<string, import('../../../diagram-api/types.js').DiagramStyleClassDef>} ClassDef styles
|
||||
* @returns {Record<string, import('../../mermaid/src/diagram-api/types.js').DiagramStyleClassDef>} ClassDef styles
|
||||
*/
|
||||
export const getClasses = function (text, diagObj) {
|
||||
log.info('Extracting classes');
|
||||
10
packages/mermaid-flowchart-elk/tsconfig.json
Normal file
10
packages/mermaid-flowchart-elk/tsconfig.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "../..",
|
||||
"outDir": "./dist",
|
||||
"types": ["vitest/importMeta", "vitest/globals"]
|
||||
},
|
||||
"include": ["./src/**/*.ts"],
|
||||
"typeRoots": ["./src/types"]
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mermaid-js/mermaid-zenuml",
|
||||
"version": "0.1.2",
|
||||
"version": "0.2.0",
|
||||
"description": "MermaidJS plugin for ZenUML integration",
|
||||
"module": "dist/mermaid-zenuml.core.mjs",
|
||||
"types": "dist/detector.d.ts",
|
||||
@@ -19,6 +19,7 @@
|
||||
"mermaid"
|
||||
],
|
||||
"scripts": {
|
||||
"clean": "rimraf dist",
|
||||
"prepublishOnly": "pnpm -w run build"
|
||||
},
|
||||
"repository": {
|
||||
@@ -33,7 +34,7 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@zenuml/core": "^3.0.6"
|
||||
"@zenuml/core": "^3.17.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"mermaid": "workspace:^"
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
* This is a dummy parser that satisfies the mermaid API logic.
|
||||
*/
|
||||
export default {
|
||||
parser: { yy: {} },
|
||||
parse: () => {
|
||||
// no op
|
||||
},
|
||||
|
||||
@@ -56,7 +56,7 @@ export const draw = async function (text: string, id: string) {
|
||||
// @ts-expect-error @zenuml/core@3.0.0 exports the wrong type for ZenUml
|
||||
const zenuml = new ZenUml(app);
|
||||
// default is a theme name. More themes to be added and will be configurable in the future
|
||||
await zenuml.render(text, 'theme-mermaid');
|
||||
await zenuml.render(text, { theme: 'default', mode: 'static' });
|
||||
|
||||
const { width, height } = window.getComputedStyle(container);
|
||||
log.debug('zenuml diagram size', width, height);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "mermaid",
|
||||
"version": "10.7.0",
|
||||
"description": "Markdown-ish syntax for generating flowcharts, sequence diagrams, class diagrams, gantt charts and git graphs.",
|
||||
"version": "11.0.0-alpha.6",
|
||||
"description": "Markdown-ish syntax for generating flowcharts, mindmaps, sequence diagrams, class diagrams, gantt charts, git graphs and more.",
|
||||
"type": "module",
|
||||
"module": "./dist/mermaid.core.mjs",
|
||||
"types": "./dist/mermaid.d.ts",
|
||||
@@ -20,7 +20,16 @@
|
||||
"sequence diagram",
|
||||
"gantt",
|
||||
"class diagram",
|
||||
"git graph"
|
||||
"git graph",
|
||||
"mindmap",
|
||||
"packet diagram",
|
||||
"c4 diagram",
|
||||
"er diagram",
|
||||
"pie chart",
|
||||
"pie diagram",
|
||||
"quadrant chart",
|
||||
"requirement diagram",
|
||||
"graph"
|
||||
],
|
||||
"scripts": {
|
||||
"clean": "rimraf dist",
|
||||
@@ -33,7 +42,7 @@
|
||||
"docs:dev": "pnpm docs:pre:vitepress && concurrently \"pnpm --filter ./src/vitepress dev\" \"tsx scripts/docs.cli.mts --watch --vitepress\"",
|
||||
"docs:dev:docker": "pnpm docs:pre:vitepress && concurrently \"pnpm --filter ./src/vitepress dev:docker\" \"tsx 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:spellcheck": "cspell \"src/docs/**/*.md\"",
|
||||
"docs:release-version": "tsx scripts/update-release-version.mts",
|
||||
"docs:verify-version": "tsx scripts/update-release-version.mts --verify",
|
||||
"types:build-config": "tsx scripts/create-types-from-json-schema.mts",
|
||||
@@ -60,25 +69,22 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@braintree/sanitize-url": "^6.0.1",
|
||||
"@types/d3-scale": "^4.0.3",
|
||||
"@types/d3-scale-chromatic": "^3.0.0",
|
||||
"cytoscape": "^3.23.0",
|
||||
"@mermaid-js/parser": "workspace:^",
|
||||
"cytoscape": "^3.28.1",
|
||||
"cytoscape-cose-bilkent": "^4.1.0",
|
||||
"cytoscape-fcose": "^2.1.0",
|
||||
"d3": "^7.4.0",
|
||||
"d3-sankey": "^0.12.3",
|
||||
"dagre-d3-es": "7.0.10",
|
||||
"dayjs": "^1.11.7",
|
||||
"dompurify": "^3.0.5",
|
||||
"elkjs": "^0.9.0",
|
||||
"katex": "^0.16.9",
|
||||
"khroma": "^2.0.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"mdast-util-from-markdown": "^1.3.0",
|
||||
"non-layered-tidy-tree-layout": "^2.0.2",
|
||||
"stylis": "^4.1.3",
|
||||
"ts-dedent": "^2.2.0",
|
||||
"uuid": "^9.0.0",
|
||||
"web-worker": "^1.2.0"
|
||||
"uuid": "^9.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@adobe/jsonschema2md": "^7.1.4",
|
||||
@@ -86,10 +92,12 @@
|
||||
"@types/d3": "^7.4.0",
|
||||
"@types/d3-sankey": "^0.12.1",
|
||||
"@types/d3-scale": "^4.0.3",
|
||||
"@types/d3-scale-chromatic": "^3.0.0",
|
||||
"@types/d3-selection": "^3.0.5",
|
||||
"@types/d3-shape": "^3.1.1",
|
||||
"@types/dompurify": "^3.0.2",
|
||||
"@types/jsdom": "^21.1.1",
|
||||
"@types/katex": "^0.16.7",
|
||||
"@types/lodash-es": "^4.17.7",
|
||||
"@types/micromatch": "^4.0.2",
|
||||
"@types/prettier": "^2.7.2",
|
||||
@@ -101,7 +109,6 @@
|
||||
"chokidar": "^3.5.3",
|
||||
"concurrently": "^8.0.1",
|
||||
"cpy-cli": "^4.2.0",
|
||||
"cspell": "^6.31.1",
|
||||
"csstree-validator": "^3.0.0",
|
||||
"globby": "^13.1.4",
|
||||
"jison": "^0.4.18",
|
||||
@@ -122,8 +129,8 @@
|
||||
"typescript": "^5.0.4",
|
||||
"unist-util-flatmap": "^1.0.0",
|
||||
"unist-util-visit": "^4.1.2",
|
||||
"vitepress": "^1.0.0-alpha.72",
|
||||
"vitepress-plugin-search": "^1.0.4-alpha.20"
|
||||
"vitepress": "^1.0.0-rc.40",
|
||||
"vitepress-plugin-search": "^1.0.4-alpha.22"
|
||||
},
|
||||
"files": [
|
||||
"dist/",
|
||||
|
||||
@@ -233,6 +233,23 @@ async function generateTypescript(mermaidConfigSchema: JSONSchemaType<MermaidCon
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Workaround for type duplication when a $ref property has siblings.
|
||||
*
|
||||
* @param json - The input JSON object.
|
||||
*
|
||||
* @see https://github.com/bcherny/json-schema-to-typescript/issues/193
|
||||
*/
|
||||
function removeProp(json: any, name: string) {
|
||||
for (const prop in json) {
|
||||
if (prop === name) {
|
||||
delete json[prop];
|
||||
} else if (typeof json[prop] === 'object') {
|
||||
removeProp(json[prop], name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Main function */
|
||||
async function main() {
|
||||
if (verifyOnly) {
|
||||
@@ -243,6 +260,8 @@ async function main() {
|
||||
|
||||
const configJsonSchema = await loadJsonSchemaFromYaml();
|
||||
|
||||
removeProp(configJsonSchema, 'default');
|
||||
|
||||
validateSchema(configJsonSchema);
|
||||
|
||||
// Generate types from JSON Schema
|
||||
|
||||
@@ -252,11 +252,12 @@ export function transformMarkdownAst({
|
||||
node.lang = MERMAID_KEYWORD;
|
||||
return [node];
|
||||
} else if (MERMAID_EXAMPLE_KEYWORDS.includes(node.lang)) {
|
||||
// Return 2 nodes:
|
||||
// If Vitepress, return only the original node with the language now set to 'mermaid-example' (will be rendered using custom renderer)
|
||||
// Else Return 2 nodes:
|
||||
// 1. the original node with the language now set to 'mermaid-example' (will be rendered as code), and
|
||||
// 2. a copy of the original node with the language set to 'mermaid' (will be rendered as a diagram)
|
||||
node.lang = MERMAID_CODE_ONLY_KEYWORD;
|
||||
return [node, Object.assign({}, node, { lang: MERMAID_KEYWORD })];
|
||||
return vitepress ? [node] : [node, Object.assign({}, node, { lang: MERMAID_KEYWORD })];
|
||||
}
|
||||
|
||||
// Transform these blocks into block quotes.
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
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 { UnknownDiagramError } from './errors.js';
|
||||
import { encodeEntities } from './utils.js';
|
||||
|
||||
import type { DetailedError } from './utils.js';
|
||||
import type { DiagramDefinition, DiagramMetadata } from './diagram-api/types.js';
|
||||
|
||||
@@ -15,48 +13,45 @@ export type ParseErrorFunction = (err: string | DetailedError | unknown, hash?:
|
||||
* @privateRemarks This is exported as part of the public mermaidAPI.
|
||||
*/
|
||||
export class Diagram {
|
||||
type = 'graph';
|
||||
parser: DiagramDefinition['parser'];
|
||||
renderer: DiagramDefinition['renderer'];
|
||||
db: DiagramDefinition['db'];
|
||||
private init?: DiagramDefinition['init'];
|
||||
|
||||
private detectError?: UnknownDiagramError;
|
||||
constructor(public text: string, public metadata: Pick<DiagramMetadata, 'title'> = {}) {
|
||||
this.text = encodeEntities(text);
|
||||
this.text += '\n';
|
||||
const cnf = configApi.getConfig();
|
||||
try {
|
||||
this.type = detectType(text, cnf);
|
||||
} catch (e) {
|
||||
this.type = 'error';
|
||||
this.detectError = e as UnknownDiagramError;
|
||||
}
|
||||
const diagram = getDiagram(this.type);
|
||||
log.debug('Type ' + this.type);
|
||||
// Setup diagram
|
||||
this.db = diagram.db;
|
||||
this.renderer = diagram.renderer;
|
||||
this.parser = diagram.parser;
|
||||
this.parser.parser.yy = this.db;
|
||||
this.init = diagram.init;
|
||||
this.parse();
|
||||
}
|
||||
|
||||
parse() {
|
||||
if (this.detectError) {
|
||||
throw this.detectError;
|
||||
}
|
||||
this.db.clear?.();
|
||||
public static async fromText(text: string, metadata: Pick<DiagramMetadata, 'title'> = {}) {
|
||||
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);
|
||||
const type = detectType(text, config);
|
||||
text = encodeEntities(text) + '\n';
|
||||
try {
|
||||
getDiagram(type);
|
||||
} catch (e) {
|
||||
const loader = getDiagramLoader(type);
|
||||
if (!loader) {
|
||||
throw new UnknownDiagramError(`Diagram ${type} not found.`);
|
||||
}
|
||||
// Diagram not available, loading it.
|
||||
// new diagram will try getDiagram again and if fails then it is a valid throw
|
||||
const { id, diagram } = await loader();
|
||||
registerDiagram(id, diagram);
|
||||
}
|
||||
this.parser.parse(this.text);
|
||||
const { db, parser, renderer, init } = getDiagram(type);
|
||||
if (parser.parser) {
|
||||
// The parser.parser.yy is only present in JISON parsers. So, we'll only set if required.
|
||||
parser.parser.yy = db;
|
||||
}
|
||||
db.clear?.();
|
||||
init?.(config);
|
||||
// This block was added for legacy compatibility. Use frontmatter instead of adding more special cases.
|
||||
if (metadata.title) {
|
||||
db.setDiagramTitle?.(metadata.title);
|
||||
}
|
||||
await parser.parse(text);
|
||||
return new Diagram(type, text, db, parser, renderer);
|
||||
}
|
||||
|
||||
private constructor(
|
||||
public type: string,
|
||||
public text: string,
|
||||
public db: DiagramDefinition['db'],
|
||||
public parser: DiagramDefinition['parser'],
|
||||
public renderer: DiagramDefinition['renderer']
|
||||
) {}
|
||||
|
||||
async render(id: string, version: string) {
|
||||
await this.renderer.draw(this.text, id, version, this);
|
||||
}
|
||||
@@ -69,34 +64,3 @@ export class Diagram {
|
||||
return this.type;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the text asynchronously and generate a Diagram object asynchronously.
|
||||
* **Warning:** This function may be changed in the future.
|
||||
* @alpha
|
||||
* @param text - The mermaid diagram definition.
|
||||
* @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,
|
||||
metadata: Pick<DiagramMetadata, 'title'> = {}
|
||||
): Promise<Diagram> => {
|
||||
const type = detectType(text, configApi.getConfig());
|
||||
try {
|
||||
// Trying to find the diagram
|
||||
getDiagram(type);
|
||||
} catch (error) {
|
||||
const loader = getDiagramLoader(type);
|
||||
if (!loader) {
|
||||
throw new UnknownDiagramError(`Diagram ${type} not found.`);
|
||||
}
|
||||
// Diagram not available, loading it.
|
||||
// new diagram will try getDiagram again and if fails then it is a valid throw
|
||||
const { id, diagram } = await loader();
|
||||
registerDiagram(id, diagram);
|
||||
}
|
||||
return new Diagram(text, metadata);
|
||||
};
|
||||
|
||||
@@ -61,7 +61,7 @@ export interface MermaidConfig {
|
||||
* You may also use `themeCSS` to override this value.
|
||||
*
|
||||
*/
|
||||
theme?: string | 'default' | 'forest' | 'dark' | 'neutral' | 'null';
|
||||
theme?: 'default' | 'forest' | 'dark' | 'neutral' | 'null';
|
||||
themeVariables?: any;
|
||||
themeCSS?: string;
|
||||
/**
|
||||
@@ -87,26 +87,11 @@ export interface MermaidConfig {
|
||||
* This option decides the amount of logging to be used by mermaid.
|
||||
*
|
||||
*/
|
||||
logLevel?:
|
||||
| number
|
||||
| string
|
||||
| 0
|
||||
| 2
|
||||
| 1
|
||||
| 'trace'
|
||||
| 'debug'
|
||||
| 'info'
|
||||
| 'warn'
|
||||
| 'error'
|
||||
| 'fatal'
|
||||
| 3
|
||||
| 4
|
||||
| 5
|
||||
| undefined;
|
||||
logLevel?: 'trace' | 0 | 'debug' | 1 | 'info' | 2 | 'warn' | 3 | 'error' | 4 | 'fatal' | 5;
|
||||
/**
|
||||
* Level of trust for parsed diagram
|
||||
*/
|
||||
securityLevel?: string | 'strict' | 'loose' | 'antiscript' | 'sandbox' | undefined;
|
||||
securityLevel?: 'strict' | 'loose' | 'antiscript' | 'sandbox';
|
||||
/**
|
||||
* Dictates whether mermaid starts on Page load
|
||||
*/
|
||||
@@ -127,6 +112,14 @@ export interface MermaidConfig {
|
||||
*
|
||||
*/
|
||||
secure?: string[];
|
||||
/**
|
||||
* This option specifies if Mermaid can expect the dependent to include KaTeX stylesheets for browsers
|
||||
* without their own MathML implementation. If this option is disabled and MathML is not supported, the math
|
||||
* equations are replaced with a warning. If this option is enabled and MathML is not supported, Mermaid will
|
||||
* fall back to legacy rendering for KaTeX.
|
||||
*
|
||||
*/
|
||||
legacyMathML?: boolean;
|
||||
/**
|
||||
* This option controls if the generated ids of nodes in the SVG are
|
||||
* generated randomly or based on a seed.
|
||||
@@ -161,6 +154,8 @@ export interface MermaidConfig {
|
||||
gitGraph?: GitGraphDiagramConfig;
|
||||
c4?: C4DiagramConfig;
|
||||
sankey?: SankeyDiagramConfig;
|
||||
packet?: PacketDiagramConfig;
|
||||
block?: BlockDiagramConfig;
|
||||
dompurifyConfig?: DOMPurifyConfiguration;
|
||||
wrap?: boolean;
|
||||
fontSize?: number;
|
||||
@@ -171,6 +166,38 @@ export interface MermaidConfig {
|
||||
*/
|
||||
suppressErrorRendering?: boolean;
|
||||
}
|
||||
/**
|
||||
* The object containing configurations specific for packet diagrams.
|
||||
*
|
||||
* This interface was referenced by `MermaidConfig`'s JSON-Schema
|
||||
* via the `definition` "PacketDiagramConfig".
|
||||
*/
|
||||
export interface PacketDiagramConfig extends BaseDiagramConfig {
|
||||
/**
|
||||
* The height of each row in the packet diagram.
|
||||
*/
|
||||
rowHeight?: number;
|
||||
/**
|
||||
* The width of each bit in the packet diagram.
|
||||
*/
|
||||
bitWidth?: number;
|
||||
/**
|
||||
* The number of bits to display per row.
|
||||
*/
|
||||
bitsPerRow?: number;
|
||||
/**
|
||||
* Toggle to display or hide bit numbers.
|
||||
*/
|
||||
showBits?: boolean;
|
||||
/**
|
||||
* The horizontal padding between the blocks in a row.
|
||||
*/
|
||||
paddingX?: number;
|
||||
/**
|
||||
* The vertical padding between the rows.
|
||||
*/
|
||||
paddingY?: number;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `MermaidConfig`'s JSON-Schema
|
||||
* via the `definition` "BaseDiagramConfig".
|
||||
@@ -185,6 +212,15 @@ export interface BaseDiagramConfig {
|
||||
*/
|
||||
useMaxWidth?: boolean;
|
||||
}
|
||||
/**
|
||||
* The object containing configurations specific for block diagrams.
|
||||
*
|
||||
* This interface was referenced by `MermaidConfig`'s JSON-Schema
|
||||
* via the `definition` "BlockDiagramConfig".
|
||||
*/
|
||||
export interface BlockDiagramConfig extends BaseDiagramConfig {
|
||||
padding?: number;
|
||||
}
|
||||
/**
|
||||
* The object containing configurations specific for c4 diagrams
|
||||
*
|
||||
@@ -795,8 +831,8 @@ export interface XYChartConfig extends BaseDiagramConfig {
|
||||
* Should show the chart title
|
||||
*/
|
||||
showTitle?: boolean;
|
||||
xAxis?: XYChartAxisConfig1;
|
||||
yAxis?: XYChartAxisConfig2;
|
||||
xAxis?: XYChartAxisConfig;
|
||||
yAxis?: XYChartAxisConfig;
|
||||
/**
|
||||
* How to plot will be drawn horizontal or vertical
|
||||
*/
|
||||
@@ -806,104 +842,6 @@ export interface XYChartConfig extends BaseDiagramConfig {
|
||||
*/
|
||||
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
|
||||
*
|
||||
@@ -924,7 +862,7 @@ export interface ErDiagramConfig extends BaseDiagramConfig {
|
||||
/**
|
||||
* Directional bias for layout of entities
|
||||
*/
|
||||
layoutDirection?: string | 'TB' | 'BT' | 'LR' | 'RL';
|
||||
layoutDirection?: 'TB' | 'BT' | 'LR' | 'RL';
|
||||
/**
|
||||
* The minimum width of an entity box. Expressed in pixels.
|
||||
*/
|
||||
@@ -989,7 +927,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?: 'dagre-d3' | 'dagre-wrapper' | 'elk';
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `MermaidConfig`'s JSON-Schema
|
||||
@@ -1013,7 +951,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?: 'dagre-d3' | 'dagre-wrapper' | 'elk';
|
||||
nodeSpacing?: number;
|
||||
rankSpacing?: number;
|
||||
/**
|
||||
@@ -1073,7 +1011,7 @@ export interface JourneyDiagramConfig extends BaseDiagramConfig {
|
||||
/**
|
||||
* Multiline message alignment
|
||||
*/
|
||||
messageAlign?: string | 'left' | 'center' | 'right';
|
||||
messageAlign?: 'left' | 'center' | 'right';
|
||||
/**
|
||||
* Prolongs the edge of the diagram downwards.
|
||||
*
|
||||
@@ -1152,7 +1090,7 @@ export interface TimelineDiagramConfig extends BaseDiagramConfig {
|
||||
/**
|
||||
* Multiline message alignment
|
||||
*/
|
||||
messageAlign?: string | 'left' | 'center' | 'right';
|
||||
messageAlign?: 'left' | 'center' | 'right';
|
||||
/**
|
||||
* Prolongs the edge of the diagram downwards.
|
||||
*
|
||||
@@ -1263,7 +1201,7 @@ export interface GanttDiagramConfig extends BaseDiagramConfig {
|
||||
* Controls the display mode.
|
||||
*
|
||||
*/
|
||||
displayMode?: string | 'compact';
|
||||
displayMode?: '' | 'compact';
|
||||
/**
|
||||
* On which day a week-based interval should start
|
||||
*
|
||||
@@ -1322,7 +1260,7 @@ export interface SequenceDiagramConfig extends BaseDiagramConfig {
|
||||
/**
|
||||
* Multiline message alignment
|
||||
*/
|
||||
messageAlign?: string | 'left' | 'center' | 'right';
|
||||
messageAlign?: 'left' | 'center' | 'right';
|
||||
/**
|
||||
* Mirror actors under diagram
|
||||
*
|
||||
@@ -1379,7 +1317,7 @@ export interface SequenceDiagramConfig extends BaseDiagramConfig {
|
||||
/**
|
||||
* This sets the text alignment of actor-attached notes
|
||||
*/
|
||||
noteAlign?: string | 'left' | 'center' | 'right';
|
||||
noteAlign?: 'left' | 'center' | 'right';
|
||||
/**
|
||||
* This sets the font size of actor messages
|
||||
*/
|
||||
@@ -1463,7 +1401,7 @@ export interface FlowchartDiagramConfig extends BaseDiagramConfig {
|
||||
* Defines how mermaid renders curves for flowcharts.
|
||||
*
|
||||
*/
|
||||
curve?: string | 'basis' | 'linear' | 'cardinal';
|
||||
curve?: 'basis' | 'linear' | 'cardinal';
|
||||
/**
|
||||
* Represents the padding between the labels and the shape
|
||||
*
|
||||
@@ -1475,7 +1413,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?: 'dagre-d3' | 'dagre-wrapper' | 'elk';
|
||||
/**
|
||||
* Width of nodes where text is wrapped.
|
||||
*
|
||||
@@ -1499,13 +1437,7 @@ export interface SankeyDiagramConfig extends BaseDiagramConfig {
|
||||
*
|
||||
*/
|
||||
linkColor?: SankeyLinkColor | string;
|
||||
/**
|
||||
* Controls the alignment of the Sankey diagrams.
|
||||
*
|
||||
* See <https://github.com/d3/d3-sankey#alignments>.
|
||||
*
|
||||
*/
|
||||
nodeAlignment?: 'left' | 'right' | 'center' | 'justify';
|
||||
nodeAlignment?: SankeyNodeAlignment;
|
||||
useMaxWidth?: boolean;
|
||||
/**
|
||||
* Toggle to display or hide values along with title.
|
||||
|
||||
@@ -24,7 +24,7 @@ flowchart
|
||||
|
||||
The new nodes C1 and C2 are a special type of nodes, clusterNodes. ClusterNodes have have the nodes in the cluster including the cluster attached in a graph object.
|
||||
|
||||
When rendering this diagram it it beeing rendered recursively. The diagram is rendered by the dagre-mermaid:render function which in turn will be used to render the node C1 and the node C2. The result of those renderings will be inserted as nodes in the "root" diagram. With this recursive approach it would be possible to have different layout direction for each cluster.
|
||||
When rendering this diagram it is being rendered recursively. The diagram is rendered by the dagre-mermaid:render function which in turn will be used to render the node C1 and the node C2. The result of those renderings will be inserted as nodes in the "root" diagram. With this recursive approach it would be possible to have different layout direction for each cluster.
|
||||
|
||||
```
|
||||
{ clusterNode: true, graph }
|
||||
|
||||
248
packages/mermaid/src/dagre-wrapper/blockArrowHelper.ts
Normal file
248
packages/mermaid/src/dagre-wrapper/blockArrowHelper.ts
Normal file
@@ -0,0 +1,248 @@
|
||||
import type { Direction } from '../../src/diagrams/block/blockTypes.js';
|
||||
|
||||
const expandAndDeduplicateDirections = (directions: Direction[]) => {
|
||||
const uniqueDirections = new Set();
|
||||
|
||||
for (const direction of directions) {
|
||||
switch (direction) {
|
||||
case 'x':
|
||||
uniqueDirections.add('right');
|
||||
uniqueDirections.add('left');
|
||||
break;
|
||||
case 'y':
|
||||
uniqueDirections.add('up');
|
||||
uniqueDirections.add('down');
|
||||
break;
|
||||
default:
|
||||
uniqueDirections.add(direction);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return uniqueDirections;
|
||||
};
|
||||
export const getArrowPoints = (
|
||||
duplicatedDirections: Direction[],
|
||||
bbox: { width: number; height: number },
|
||||
node: any
|
||||
) => {
|
||||
// Expand and deduplicate the provided directions.
|
||||
// for instance: x, right => right, left
|
||||
const directions = expandAndDeduplicateDirections(duplicatedDirections);
|
||||
|
||||
// Factor to divide height for some calculations.
|
||||
const f = 2;
|
||||
|
||||
// Calculated height of the bounding box, accounting for node padding.
|
||||
const height = bbox.height + 2 * node.padding;
|
||||
// Midpoint calculation based on height.
|
||||
const midpoint = height / f;
|
||||
// Calculated width of the bounding box, accounting for additional width and node padding.
|
||||
const width = bbox.width + 2 * midpoint + node.padding;
|
||||
// Padding to use, half of the node padding.
|
||||
const padding = node.padding / 2;
|
||||
|
||||
// Initialize an empty array to store points for the arrow.
|
||||
const points = [];
|
||||
|
||||
if (
|
||||
directions.has('right') &&
|
||||
directions.has('left') &&
|
||||
directions.has('up') &&
|
||||
directions.has('down')
|
||||
) {
|
||||
// SQUARE
|
||||
return [
|
||||
// Bottom
|
||||
{ x: 0, y: 0 },
|
||||
{ x: midpoint, y: 0 },
|
||||
{ x: width / 2, y: 2 * padding },
|
||||
{ x: width - midpoint, y: 0 },
|
||||
{ x: width, y: 0 },
|
||||
|
||||
// Right
|
||||
{ x: width, y: -height / 3 },
|
||||
{ x: width + 2 * padding, y: -height / 2 },
|
||||
{ x: width, y: (-2 * height) / 3 },
|
||||
{ x: width, y: -height },
|
||||
|
||||
// Top
|
||||
{ x: width - midpoint, y: -height },
|
||||
{ x: width / 2, y: -height - 2 * padding },
|
||||
{ x: midpoint, y: -height },
|
||||
|
||||
// Left
|
||||
{ x: 0, y: -height },
|
||||
{ x: 0, y: (-2 * height) / 3 },
|
||||
{ x: -2 * padding, y: -height / 2 },
|
||||
{ x: 0, y: -height / 3 },
|
||||
];
|
||||
}
|
||||
if (directions.has('right') && directions.has('left') && directions.has('up')) {
|
||||
// RECTANGLE_VERTICAL (Top Open)
|
||||
return [
|
||||
{ x: midpoint, y: 0 },
|
||||
{ x: width - midpoint, y: 0 },
|
||||
{ x: width, y: -height / 2 },
|
||||
{ x: width - midpoint, y: -height },
|
||||
{ x: midpoint, y: -height },
|
||||
{ x: 0, y: -height / 2 },
|
||||
];
|
||||
}
|
||||
if (directions.has('right') && directions.has('left') && directions.has('down')) {
|
||||
// RECTANGLE_VERTICAL (Bottom Open)
|
||||
return [
|
||||
{ x: 0, y: 0 },
|
||||
{ x: midpoint, y: -height },
|
||||
{ x: width - midpoint, y: -height },
|
||||
{ x: width, y: 0 },
|
||||
];
|
||||
}
|
||||
if (directions.has('right') && directions.has('up') && directions.has('down')) {
|
||||
// RECTANGLE_HORIZONTAL (Right Open)
|
||||
return [
|
||||
{ x: 0, y: 0 },
|
||||
{ x: width, y: -midpoint },
|
||||
{ x: width, y: -height + midpoint },
|
||||
{ x: 0, y: -height },
|
||||
];
|
||||
}
|
||||
if (directions.has('left') && directions.has('up') && directions.has('down')) {
|
||||
// RECTANGLE_HORIZONTAL (Left Open)
|
||||
return [
|
||||
{ x: width, y: 0 },
|
||||
{ x: 0, y: -midpoint },
|
||||
{ x: 0, y: -height + midpoint },
|
||||
{ x: width, y: -height },
|
||||
];
|
||||
}
|
||||
if (directions.has('right') && directions.has('left')) {
|
||||
// HORIZONTAL_LINE
|
||||
return [
|
||||
{ x: midpoint, y: 0 },
|
||||
{ x: midpoint, y: -padding },
|
||||
{ x: width - midpoint, y: -padding },
|
||||
{ x: width - midpoint, y: 0 },
|
||||
{ x: width, y: -height / 2 },
|
||||
{ x: width - midpoint, y: -height },
|
||||
{ x: width - midpoint, y: -height + padding },
|
||||
{ x: midpoint, y: -height + padding },
|
||||
{ x: midpoint, y: -height },
|
||||
{ x: 0, y: -height / 2 },
|
||||
];
|
||||
}
|
||||
if (directions.has('up') && directions.has('down')) {
|
||||
// VERTICAL_LINE
|
||||
return [
|
||||
// Bottom center
|
||||
{ x: width / 2, y: 0 },
|
||||
// Left pont of bottom arrow
|
||||
{ x: 0, y: -padding },
|
||||
{ x: midpoint, y: -padding },
|
||||
// Left top over vertical section
|
||||
{ x: midpoint, y: -height + padding },
|
||||
{ x: 0, y: -height + padding },
|
||||
// Top of arrow
|
||||
{ x: width / 2, y: -height },
|
||||
{ x: width, y: -height + padding },
|
||||
// Top of right vertical bar
|
||||
{ x: width - midpoint, y: -height + padding },
|
||||
{ x: width - midpoint, y: -padding },
|
||||
{ x: width, y: -padding },
|
||||
];
|
||||
}
|
||||
if (directions.has('right') && directions.has('up')) {
|
||||
// ANGLE_RT
|
||||
return [
|
||||
{ x: 0, y: 0 },
|
||||
{ x: width, y: -midpoint },
|
||||
{ x: 0, y: -height },
|
||||
];
|
||||
}
|
||||
if (directions.has('right') && directions.has('down')) {
|
||||
// ANGLE_RB
|
||||
return [
|
||||
{ x: 0, y: 0 },
|
||||
{ x: width, y: 0 },
|
||||
{ x: 0, y: -height },
|
||||
];
|
||||
}
|
||||
if (directions.has('left') && directions.has('up')) {
|
||||
// ANGLE_LT
|
||||
return [
|
||||
{ x: width, y: 0 },
|
||||
{ x: 0, y: -midpoint },
|
||||
{ x: width, y: -height },
|
||||
];
|
||||
}
|
||||
if (directions.has('left') && directions.has('down')) {
|
||||
// ANGLE_LB
|
||||
return [
|
||||
{ x: width, y: 0 },
|
||||
{ x: 0, y: 0 },
|
||||
{ x: width, y: -height },
|
||||
];
|
||||
}
|
||||
if (directions.has('right')) {
|
||||
// ARROW_RIGHT
|
||||
return [
|
||||
{ x: midpoint, y: -padding },
|
||||
{ x: midpoint, y: -padding },
|
||||
{ x: width - midpoint, y: -padding },
|
||||
{ x: width - midpoint, y: 0 },
|
||||
{ x: width, y: -height / 2 },
|
||||
{ x: width - midpoint, y: -height },
|
||||
{ x: width - midpoint, y: -height + padding },
|
||||
// top left corner of arrow
|
||||
{ x: midpoint, y: -height + padding },
|
||||
{ x: midpoint, y: -height + padding },
|
||||
];
|
||||
}
|
||||
if (directions.has('left')) {
|
||||
// ARROW_LEFT
|
||||
return [
|
||||
{ x: midpoint, y: 0 },
|
||||
{ x: midpoint, y: -padding },
|
||||
// Two points, the right corners
|
||||
{ x: width - midpoint, y: -padding },
|
||||
{ x: width - midpoint, y: -height + padding },
|
||||
{ x: midpoint, y: -height + padding },
|
||||
{ x: midpoint, y: -height },
|
||||
{ x: 0, y: -height / 2 },
|
||||
];
|
||||
}
|
||||
if (directions.has('up')) {
|
||||
// ARROW_TOP
|
||||
return [
|
||||
// Bottom center
|
||||
{ x: midpoint, y: -padding },
|
||||
// Left top over vertical section
|
||||
{ x: midpoint, y: -height + padding },
|
||||
{ x: 0, y: -height + padding },
|
||||
// Top of arrow
|
||||
{ x: width / 2, y: -height },
|
||||
{ x: width, y: -height + padding },
|
||||
// Top of right vertical bar
|
||||
{ x: width - midpoint, y: -height + padding },
|
||||
{ x: width - midpoint, y: -padding },
|
||||
];
|
||||
}
|
||||
if (directions.has('down')) {
|
||||
// ARROW_BOTTOM
|
||||
return [
|
||||
// Bottom center
|
||||
{ x: width / 2, y: 0 },
|
||||
// Left pont of bottom arrow
|
||||
{ x: 0, y: -padding },
|
||||
{ x: midpoint, y: -padding },
|
||||
// Left top over vertical section
|
||||
{ x: midpoint, y: -height + padding },
|
||||
{ x: width - midpoint, y: -height + padding },
|
||||
{ x: width - midpoint, y: -padding },
|
||||
{ x: width, y: -padding },
|
||||
];
|
||||
}
|
||||
|
||||
// POINT
|
||||
return [{ x: 0, y: 0 }];
|
||||
};
|
||||
@@ -69,13 +69,13 @@ const rect = (parent, node) => {
|
||||
if (useHtmlLabels) {
|
||||
label.attr(
|
||||
'transform',
|
||||
// This puts the labal on top of the box instead of inside it
|
||||
// This puts the label on top of the box instead of inside it
|
||||
`translate(${node.x - bbox.width / 2}, ${node.y - node.height / 2 + subGraphTitleTopMargin})`
|
||||
);
|
||||
} else {
|
||||
label.attr(
|
||||
'transform',
|
||||
// This puts the labal on top of the box instead of inside it
|
||||
// This puts the label on top of the box instead of inside it
|
||||
`translate(${node.x}, ${node.y - node.height / 2 + subGraphTitleTopMargin})`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -56,11 +56,11 @@ const createLabel = (_vertexText, style, isTitle, isNode) => {
|
||||
if (evaluate(getConfig().flowchart.htmlLabels)) {
|
||||
// TODO: addHtmlLabel accepts a labelStyle. Do we possibly have that?
|
||||
vertexText = vertexText.replace(/\\n|\n/g, '<br />');
|
||||
log.info('vertexText' + vertexText);
|
||||
log.debug('vertexText' + vertexText);
|
||||
const node = {
|
||||
isNode,
|
||||
label: decodeEntities(vertexText).replace(
|
||||
/fa[blrs]?:fa-[\w-]+/g,
|
||||
/fa[blrs]?:fa-[\w-]+/g, // cspell: disable-line
|
||||
(s) => `<i class='${s.replace(':', ' ')}'></i>`
|
||||
),
|
||||
labelStyle: style.replace('fill:', 'color:'),
|
||||
|
||||
@@ -28,7 +28,6 @@ export const insertEdgeLabel = (elem, edge) => {
|
||||
addSvgBackground: true,
|
||||
})
|
||||
: createLabel(edge.label, edge.labelStyle);
|
||||
log.info('abc82', edge, edge.labelType);
|
||||
|
||||
// Create outer g, edgeLabel, this will be positioned after graph layout
|
||||
const edgeLabel = elem.insert('g').attr('class', 'edgeLabel');
|
||||
@@ -135,7 +134,7 @@ function setTerminalWidth(fo, value) {
|
||||
}
|
||||
|
||||
export const positionEdgeLabel = (edge, paths) => {
|
||||
log.info('Moving label abc78 ', edge.id, edge.label, edgeLabels[edge.id]);
|
||||
log.debug('Moving label abc88 ', edge.id, edge.label, edgeLabels[edge.id], paths);
|
||||
let path = paths.updatedPath ? paths.updatedPath : paths.originalPath;
|
||||
const siteConfig = getConfig();
|
||||
const { subGraphTitleTotalMargin } = getSubGraphTitleMargins(siteConfig);
|
||||
@@ -146,7 +145,7 @@ export const positionEdgeLabel = (edge, paths) => {
|
||||
if (path) {
|
||||
// // debugger;
|
||||
const pos = utils.calcLabelPosition(path);
|
||||
log.info(
|
||||
log.debug(
|
||||
'Moving label ' + edge.label + ' from (',
|
||||
x,
|
||||
',',
|
||||
@@ -155,7 +154,7 @@ export const positionEdgeLabel = (edge, paths) => {
|
||||
pos.x,
|
||||
',',
|
||||
pos.y,
|
||||
') abc78'
|
||||
') abc88'
|
||||
);
|
||||
if (paths.updatedPath) {
|
||||
x = pos.x;
|
||||
@@ -221,7 +220,6 @@ export const positionEdgeLabel = (edge, paths) => {
|
||||
};
|
||||
|
||||
const outsideNode = (node, point) => {
|
||||
// log.warn('Checking bounds ', node, point);
|
||||
const x = node.x;
|
||||
const y = node.y;
|
||||
const dx = Math.abs(point.x - x);
|
||||
@@ -235,7 +233,7 @@ const outsideNode = (node, point) => {
|
||||
};
|
||||
|
||||
export const intersection = (node, outsidePoint, insidePoint) => {
|
||||
log.warn(`intersection calc abc89:
|
||||
log.debug(`intersection calc abc89:
|
||||
outsidePoint: ${JSON.stringify(outsidePoint)}
|
||||
insidePoint : ${JSON.stringify(insidePoint)}
|
||||
node : x:${node.x} y:${node.y} w:${node.width} h:${node.height}`);
|
||||
@@ -248,29 +246,11 @@ export const intersection = (node, outsidePoint, insidePoint) => {
|
||||
let r = insidePoint.x < outsidePoint.x ? w - dx : w + dx;
|
||||
const h = node.height / 2;
|
||||
|
||||
// const edges = {
|
||||
// x1: x - w,
|
||||
// x2: x + w,
|
||||
// y1: y - h,
|
||||
// y2: y + h
|
||||
// };
|
||||
|
||||
// if (
|
||||
// outsidePoint.x === edges.x1 ||
|
||||
// outsidePoint.x === edges.x2 ||
|
||||
// outsidePoint.y === edges.y1 ||
|
||||
// outsidePoint.y === edges.y2
|
||||
// ) {
|
||||
// log.warn('abc89 calc equals on edge', outsidePoint, edges);
|
||||
// return outsidePoint;
|
||||
// }
|
||||
|
||||
const Q = Math.abs(outsidePoint.y - insidePoint.y);
|
||||
const R = Math.abs(outsidePoint.x - insidePoint.x);
|
||||
// log.warn();
|
||||
|
||||
if (Math.abs(y - outsidePoint.y) * w > Math.abs(x - outsidePoint.x) * h) {
|
||||
// Intersection is top or bottom of rect.
|
||||
// let q = insidePoint.y < outsidePoint.y ? outsidePoint.y - h - y : y - h - outsidePoint.y;
|
||||
let q = insidePoint.y < outsidePoint.y ? outsidePoint.y - h - y : y - h - outsidePoint.y;
|
||||
r = (R * q) / Q;
|
||||
const res = {
|
||||
@@ -289,7 +269,7 @@ export const intersection = (node, outsidePoint, insidePoint) => {
|
||||
res.y = outsidePoint.y;
|
||||
}
|
||||
|
||||
log.warn(`abc89 topp/bott calc, Q ${Q}, q ${q}, R ${R}, r ${r}`, res);
|
||||
log.debug(`abc89 topp/bott calc, Q ${Q}, q ${q}, R ${R}, r ${r}`, res); // cspell: disable-line
|
||||
|
||||
return res;
|
||||
} else {
|
||||
@@ -306,7 +286,7 @@ export const intersection = (node, outsidePoint, insidePoint) => {
|
||||
let _x = insidePoint.x < outsidePoint.x ? insidePoint.x + R - r : insidePoint.x - R + r;
|
||||
// let _x = insidePoint.x < outsidePoint.x ? insidePoint.x + R - r : outsidePoint.x + r;
|
||||
let _y = insidePoint.y < outsidePoint.y ? insidePoint.y + q : insidePoint.y - q;
|
||||
log.warn(`sides calc abc89, Q ${Q}, q ${q}, R ${R}, r ${r}`, { _x, _y });
|
||||
log.debug(`sides calc abc89, Q ${Q}, q ${q}, R ${R}, r ${r}`, { _x, _y });
|
||||
if (r === 0) {
|
||||
_x = outsidePoint.x;
|
||||
_y = outsidePoint.y;
|
||||
@@ -326,25 +306,20 @@ export const intersection = (node, outsidePoint, insidePoint) => {
|
||||
* and return an update path ending by the border of the node.
|
||||
*
|
||||
* @param {Array} _points
|
||||
* @param {any} boundryNode
|
||||
* @param {any} boundaryNode
|
||||
* @returns {Array} Points
|
||||
*/
|
||||
const cutPathAtIntersect = (_points, boundryNode) => {
|
||||
log.warn('abc88 cutPathAtIntersect', _points, boundryNode);
|
||||
const cutPathAtIntersect = (_points, boundaryNode) => {
|
||||
log.debug('abc88 cutPathAtIntersect', _points, boundaryNode);
|
||||
let points = [];
|
||||
let lastPointOutside = _points[0];
|
||||
let isInside = false;
|
||||
_points.forEach((point) => {
|
||||
// const node = clusterDb[edge.toCluster].node;
|
||||
log.info('abc88 checking point', point, boundryNode);
|
||||
|
||||
// check if point is inside the boundary rect
|
||||
if (!outsideNode(boundryNode, point) && !isInside) {
|
||||
if (!outsideNode(boundaryNode, point) && !isInside) {
|
||||
// First point inside the rect found
|
||||
// Calc the intersection coord between the point anf the last point outside the rect
|
||||
const inter = intersection(boundryNode, lastPointOutside, point);
|
||||
log.warn('abc88 inside', point, lastPointOutside, inter);
|
||||
log.warn('abc88 intersection', inter);
|
||||
const inter = intersection(boundaryNode, lastPointOutside, point);
|
||||
|
||||
// // Check case where the intersection is the same as the last point
|
||||
let pointPresent = false;
|
||||
@@ -354,14 +329,11 @@ const cutPathAtIntersect = (_points, boundryNode) => {
|
||||
// // if (!pointPresent) {
|
||||
if (!points.some((e) => e.x === inter.x && e.y === inter.y)) {
|
||||
points.push(inter);
|
||||
} else {
|
||||
log.warn('abc88 no intersect', inter, points);
|
||||
}
|
||||
// points.push(inter);
|
||||
|
||||
isInside = true;
|
||||
} else {
|
||||
// Outside
|
||||
log.warn('abc88 outside', point, lastPointOutside);
|
||||
lastPointOutside = point;
|
||||
// points.push(point);
|
||||
if (!isInside) {
|
||||
@@ -369,67 +341,31 @@ const cutPathAtIntersect = (_points, boundryNode) => {
|
||||
}
|
||||
}
|
||||
});
|
||||
log.warn('abc88 returning points', points);
|
||||
return points;
|
||||
};
|
||||
|
||||
export const insertEdge = function (elem, e, edge, clusterDb, diagramType, graph, id) {
|
||||
let points = edge.points;
|
||||
log.debug('abc88 InsertEdge: edge=', edge, 'e=', e);
|
||||
let pointsHasChanged = false;
|
||||
const tail = graph.node(e.v);
|
||||
var head = graph.node(e.w);
|
||||
|
||||
log.info('abc88 InsertEdge: ', edge);
|
||||
if (head.intersect && tail.intersect) {
|
||||
if (head?.intersect && tail?.intersect) {
|
||||
points = points.slice(1, edge.points.length - 1);
|
||||
points.unshift(tail.intersect(points[0]));
|
||||
log.info(
|
||||
'Last point',
|
||||
points[points.length - 1],
|
||||
head,
|
||||
head.intersect(points[points.length - 1])
|
||||
);
|
||||
points.push(head.intersect(points[points.length - 1]));
|
||||
}
|
||||
|
||||
if (edge.toCluster) {
|
||||
log.info('to cluster abc88', clusterDb[edge.toCluster]);
|
||||
log.debug('to cluster abc88', clusterDb[edge.toCluster]);
|
||||
points = cutPathAtIntersect(edge.points, clusterDb[edge.toCluster].node);
|
||||
// log.trace('edge', edge);
|
||||
// points = [];
|
||||
// let lastPointOutside; // = edge.points[0];
|
||||
// let isInside = false;
|
||||
// edge.points.forEach(point => {
|
||||
// const node = clusterDb[edge.toCluster].node;
|
||||
// log.warn('checking from', edge.fromCluster, point, node);
|
||||
|
||||
// if (!outsideNode(node, point) && !isInside) {
|
||||
// log.trace('inside', edge.toCluster, point, lastPointOutside);
|
||||
|
||||
// // First point inside the rect
|
||||
// const inter = intersection(node, lastPointOutside, point);
|
||||
|
||||
// let pointPresent = false;
|
||||
// points.forEach(p => {
|
||||
// pointPresent = pointPresent || (p.x === inter.x && p.y === inter.y);
|
||||
// });
|
||||
// // if (!pointPresent) {
|
||||
// if (!points.find(e => e.x === inter.x && e.y === inter.y)) {
|
||||
// points.push(inter);
|
||||
// } else {
|
||||
// log.warn('no intersect', inter, points);
|
||||
// }
|
||||
// isInside = true;
|
||||
// } else {
|
||||
// // outside
|
||||
// lastPointOutside = point;
|
||||
// if (!isInside) points.push(point);
|
||||
// }
|
||||
// });
|
||||
pointsHasChanged = true;
|
||||
}
|
||||
|
||||
if (edge.fromCluster) {
|
||||
log.info('from cluster abc88', clusterDb[edge.fromCluster]);
|
||||
log.debug('from cluster abc88', clusterDb[edge.fromCluster]);
|
||||
points = cutPathAtIntersect(points.reverse(), clusterDb[edge.fromCluster].node).reverse();
|
||||
|
||||
pointsHasChanged = true;
|
||||
@@ -507,8 +443,6 @@ export const insertEdge = function (elem, e, edge, clusterDb, diagramType, graph
|
||||
url = url.replace(/\(/g, '\\(');
|
||||
url = url.replace(/\)/g, '\\)');
|
||||
}
|
||||
log.info('arrowTypeStart', edge.arrowTypeStart);
|
||||
log.info('arrowTypeEnd', edge.arrowTypeEnd);
|
||||
|
||||
addEdgeMarkers(svgPath, edge, url, id, diagramType);
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ import { log } from '../logger.js';
|
||||
import { getSubGraphTitleMargins } from '../utils/subGraphTitleMargins.js';
|
||||
import { getConfig } from '../diagram-api/diagramAPI.js';
|
||||
|
||||
const recursiveRender = async (_elem, graph, diagramtype, id, parentCluster, siteConfig) => {
|
||||
const recursiveRender = async (_elem, graph, diagramType, id, parentCluster, siteConfig) => {
|
||||
log.info('Graph in recursive render: XXX', graphlibJson.write(graph), parentCluster);
|
||||
const dir = graph.graph().rankdir;
|
||||
log.trace('Dir in recursive render - dir:', dir);
|
||||
@@ -57,7 +57,7 @@ const recursiveRender = async (_elem, graph, diagramtype, id, parentCluster, sit
|
||||
const o = await recursiveRender(
|
||||
nodes,
|
||||
node.graph,
|
||||
diagramtype,
|
||||
diagramType,
|
||||
id,
|
||||
graph.node(v),
|
||||
siteConfig
|
||||
@@ -95,7 +95,7 @@ const recursiveRender = async (_elem, graph, diagramtype, id, parentCluster, sit
|
||||
log.info('Edge ' + e.v + ' -> ' + e.w + ': ', e, ' ', JSON.stringify(graph.edge(e)));
|
||||
|
||||
// Check if link is either from or to a cluster
|
||||
log.info('Fix', clusterDb, 'ids:', e.v, e.w, 'Translateing: ', clusterDb[e.v], clusterDb[e.w]);
|
||||
log.info('Fix', clusterDb, 'ids:', e.v, e.w, 'Translating: ', clusterDb[e.v], clusterDb[e.w]);
|
||||
insertEdgeLabel(edgeLabels, edge);
|
||||
});
|
||||
|
||||
@@ -147,7 +147,7 @@ const recursiveRender = async (_elem, graph, diagramtype, id, parentCluster, sit
|
||||
log.info('Edge ' + e.v + ' -> ' + e.w + ': ' + JSON.stringify(edge), edge);
|
||||
|
||||
edge.points.forEach((point) => (point.y += subGraphTitleTotalMargin / 2));
|
||||
const paths = insertEdge(edgePaths, e, edge, clusterDb, diagramtype, graph, id);
|
||||
const paths = insertEdge(edgePaths, e, edge, clusterDb, diagramType, graph, id);
|
||||
positionEdgeLabel(edge, paths);
|
||||
});
|
||||
|
||||
@@ -161,8 +161,8 @@ const recursiveRender = async (_elem, graph, diagramtype, id, parentCluster, sit
|
||||
return { elem, diff };
|
||||
};
|
||||
|
||||
export const render = async (elem, graph, markers, diagramtype, id) => {
|
||||
insertMarkers(elem, markers, diagramtype, id);
|
||||
export const render = async (elem, graph, markers, diagramType, id) => {
|
||||
insertMarkers(elem, markers, diagramType, id);
|
||||
clearNodes();
|
||||
clearEdges();
|
||||
clearClusters();
|
||||
@@ -173,7 +173,7 @@ export const render = async (elem, graph, markers, diagramtype, id) => {
|
||||
log.warn('Graph after:', JSON.stringify(graphlibJson.write(graph)));
|
||||
// log.warn('Graph ever after:', graphlibJson.write(graph.node('A').graph));
|
||||
const siteConfig = getConfig();
|
||||
await recursiveRender(elem, graph, diagramtype, id, undefined, siteConfig);
|
||||
await recursiveRender(elem, graph, diagramType, id, undefined, siteConfig);
|
||||
};
|
||||
|
||||
// const shapeDefinitions = {};
|
||||
|
||||
@@ -13,11 +13,11 @@ export const clear = () => {
|
||||
clusterDb = {};
|
||||
};
|
||||
|
||||
const isDescendant = (id, ancenstorId) => {
|
||||
// if (id === ancenstorId) return true;
|
||||
const isDescendant = (id, ancestorId) => {
|
||||
// if (id === ancestorId) return true;
|
||||
|
||||
log.trace('In isDecendant', ancenstorId, ' ', id, ' = ', descendants[ancenstorId].includes(id));
|
||||
if (descendants[ancenstorId].includes(id)) {
|
||||
log.trace('In isDescendant', ancestorId, ' ', id, ' = ', descendants[ancestorId].includes(id));
|
||||
if (descendants[ancestorId].includes(id)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ const isDescendant = (id, ancenstorId) => {
|
||||
};
|
||||
|
||||
const edgeInCluster = (edge, clusterId) => {
|
||||
log.info('Decendants of ', clusterId, ' is ', descendants[clusterId]);
|
||||
log.info('Descendants of ', clusterId, ' is ', descendants[clusterId]);
|
||||
log.info('Edge is ', edge);
|
||||
// Edges to/from the cluster is not in the cluster, they are in the parent
|
||||
if (edge.v === clusterId) {
|
||||
@@ -36,7 +36,7 @@ const edgeInCluster = (edge, clusterId) => {
|
||||
}
|
||||
|
||||
if (!descendants[clusterId]) {
|
||||
log.debug('Tilt, ', clusterId, ',not in decendants');
|
||||
log.debug('Tilt, ', clusterId, ',not in descendants');
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
@@ -244,7 +244,7 @@ export const adjustClustersAndEdges = (graph, depth) => {
|
||||
// d1 xor d2 - if either d1 is true and d2 is false or the other way around
|
||||
if (d1 ^ d2) {
|
||||
log.warn('Edge: ', edge, ' leaves cluster ', id);
|
||||
log.warn('Decendants of XXX ', id, ': ', descendants[id]);
|
||||
log.warn('Descendants of XXX ', id, ': ', descendants[id]);
|
||||
clusterDb[id].externalConnections = true;
|
||||
}
|
||||
}
|
||||
@@ -286,6 +286,7 @@ export const adjustClustersAndEdges = (graph, depth) => {
|
||||
clusterDb[e.w]
|
||||
);
|
||||
if (clusterDb[e.v] && clusterDb[e.w] && clusterDb[e.v] === clusterDb[e.w]) {
|
||||
// cspell:ignore trixing
|
||||
log.warn('Fixing and trixing link to self - removing XXX', e.v, e.w, e.name);
|
||||
log.warn('Fixing and trixing - removing XXX', e.v, e.w, e.name);
|
||||
v = getAnchorId(e.v);
|
||||
@@ -337,7 +338,7 @@ export const adjustClustersAndEdges = (graph, depth) => {
|
||||
|
||||
// Remove references to extracted cluster
|
||||
// graph.edges().forEach(edge => {
|
||||
// if (isDecendant(edge.v, clusterId) || isDecendant(edge.w, clusterId)) {
|
||||
// if (isDescendant(edge.v, clusterId) || isDescendant(edge.w, clusterId)) {
|
||||
// graph.removeEdge(edge);
|
||||
// }
|
||||
// });
|
||||
|
||||
@@ -6,6 +6,7 @@ import intersect from './intersect/index.js';
|
||||
import createLabel from './createLabel.js';
|
||||
import note from './shapes/note.js';
|
||||
import { evaluate } from '../diagrams/common/common.js';
|
||||
import { getArrowPoints } from './blockArrowHelper.js';
|
||||
|
||||
const formatClass = (str) => {
|
||||
if (str) {
|
||||
@@ -30,6 +31,7 @@ const question = async (parent, node) => {
|
||||
const w = bbox.width + node.padding;
|
||||
const h = bbox.height + node.padding;
|
||||
const s = w + h;
|
||||
|
||||
const points = [
|
||||
{ x: s / 2, y: 0 },
|
||||
{ x: s, y: -s / 2 },
|
||||
@@ -117,6 +119,27 @@ const hexagon = async (parent, node) => {
|
||||
return shapeSvg;
|
||||
};
|
||||
|
||||
const block_arrow = async (parent, node) => {
|
||||
const { shapeSvg, bbox } = await labelHelper(parent, node, undefined, true);
|
||||
|
||||
const f = 2;
|
||||
const h = bbox.height + 2 * node.padding;
|
||||
const m = h / f;
|
||||
const w = bbox.width + 2 * m + node.padding;
|
||||
|
||||
const points = getArrowPoints(node.directions, bbox, node);
|
||||
|
||||
const blockArrow = insertPolygonShape(shapeSvg, w, h, points);
|
||||
blockArrow.attr('style', node.style);
|
||||
updateNodeBounds(node, blockArrow);
|
||||
|
||||
node.intersect = function (point) {
|
||||
return intersect.polygon(node, points, point);
|
||||
};
|
||||
|
||||
return shapeSvg;
|
||||
};
|
||||
|
||||
const rect_left_inv_arrow = async (parent, node) => {
|
||||
const { shapeSvg, bbox } = await labelHelper(
|
||||
parent,
|
||||
@@ -374,17 +397,64 @@ const rect = async (parent, node) => {
|
||||
|
||||
// const totalWidth = bbox.width + node.padding * 2;
|
||||
// const totalHeight = bbox.height + node.padding * 2;
|
||||
const totalWidth = bbox.width + node.padding;
|
||||
const totalHeight = bbox.height + node.padding;
|
||||
const totalWidth = node.positioned ? node.width : bbox.width + node.padding;
|
||||
const totalHeight = node.positioned ? node.height : bbox.height + node.padding;
|
||||
const x = node.positioned ? -totalWidth / 2 : -bbox.width / 2 - halfPadding;
|
||||
const y = node.positioned ? -totalHeight / 2 : -bbox.height / 2 - halfPadding;
|
||||
rect
|
||||
.attr('class', 'basic label-container')
|
||||
.attr('style', node.style)
|
||||
.attr('rx', node.rx)
|
||||
.attr('ry', node.ry)
|
||||
// .attr('x', -bbox.width / 2 - node.padding)
|
||||
// .attr('y', -bbox.height / 2 - node.padding)
|
||||
.attr('x', -bbox.width / 2 - halfPadding)
|
||||
.attr('y', -bbox.height / 2 - halfPadding)
|
||||
.attr('x', x)
|
||||
.attr('y', y)
|
||||
.attr('width', totalWidth)
|
||||
.attr('height', totalHeight);
|
||||
|
||||
if (node.props) {
|
||||
const propKeys = new Set(Object.keys(node.props));
|
||||
if (node.props.borders) {
|
||||
applyNodePropertyBorders(rect, node.props.borders, totalWidth, totalHeight);
|
||||
propKeys.delete('borders');
|
||||
}
|
||||
propKeys.forEach((propKey) => {
|
||||
log.warn(`Unknown node property ${propKey}`);
|
||||
});
|
||||
}
|
||||
|
||||
updateNodeBounds(node, rect);
|
||||
|
||||
node.intersect = function (point) {
|
||||
return intersect.rect(node, point);
|
||||
};
|
||||
|
||||
return shapeSvg;
|
||||
};
|
||||
|
||||
const composite = async (parent, node) => {
|
||||
const { shapeSvg, bbox, halfPadding } = await labelHelper(
|
||||
parent,
|
||||
node,
|
||||
'node ' + node.classes,
|
||||
true
|
||||
);
|
||||
|
||||
// add the rect
|
||||
const rect = shapeSvg.insert('rect', ':first-child');
|
||||
|
||||
// const totalWidth = bbox.width + node.padding * 2;
|
||||
// const totalHeight = bbox.height + node.padding * 2;
|
||||
const totalWidth = node.positioned ? node.width : bbox.width + node.padding;
|
||||
const totalHeight = node.positioned ? node.height : bbox.height + node.padding;
|
||||
const x = node.positioned ? -totalWidth / 2 : -bbox.width / 2 - halfPadding;
|
||||
const y = node.positioned ? -totalHeight / 2 : -bbox.height / 2 - halfPadding;
|
||||
rect
|
||||
.attr('class', 'basic cluster composite label-container')
|
||||
.attr('style', node.style)
|
||||
.attr('rx', node.rx)
|
||||
.attr('ry', node.ry)
|
||||
.attr('x', x)
|
||||
.attr('y', y)
|
||||
.attr('width', totalWidth)
|
||||
.attr('height', totalHeight);
|
||||
|
||||
@@ -1031,6 +1101,7 @@ const class_box = (parent, node) => {
|
||||
|
||||
const shapes = {
|
||||
rhombus: question,
|
||||
composite,
|
||||
question,
|
||||
rect,
|
||||
labelRect,
|
||||
@@ -1040,6 +1111,7 @@ const shapes = {
|
||||
doublecircle,
|
||||
stadium,
|
||||
hexagon,
|
||||
block_arrow,
|
||||
rect_left_inv_arrow,
|
||||
lean_right,
|
||||
lean_left,
|
||||
@@ -1082,6 +1154,9 @@ export const insertNode = async (elem, node, dir) => {
|
||||
if (node.class) {
|
||||
el.attr('class', 'node default ' + node.class);
|
||||
}
|
||||
// MC Special
|
||||
newEl.attr('data-node', 'true');
|
||||
newEl.attr('data-id', node.id);
|
||||
|
||||
nodeElems[node.id] = newEl;
|
||||
|
||||
@@ -1099,7 +1174,6 @@ export const clear = () => {
|
||||
|
||||
export const positionNode = (node) => {
|
||||
const el = nodeElems[node.id];
|
||||
|
||||
log.trace(
|
||||
'Transforming node',
|
||||
node.diff,
|
||||
|
||||
@@ -115,6 +115,7 @@ export const labelHelper = async (parent, node, _classes, isNode) => {
|
||||
label.attr('transform', 'translate(' + -bbox.width / 2 + ', ' + -bbox.height / 2 + ')');
|
||||
}
|
||||
label.insert('rect', ':first-child');
|
||||
|
||||
return { shapeSvg, bbox, halfPadding, label };
|
||||
};
|
||||
|
||||
|
||||
@@ -257,6 +257,9 @@ const config: RequiredDeep<MermaidConfig> = {
|
||||
// TODO: can we make this default to `true` instead?
|
||||
useMaxWidth: false,
|
||||
},
|
||||
packet: {
|
||||
...defaultConfigJson.packet,
|
||||
},
|
||||
};
|
||||
|
||||
const keyify = (obj: any, prefix = ''): string[] =>
|
||||
|
||||
@@ -71,10 +71,9 @@ export const registerLazyLoadedDiagrams = (...diagrams: ExternalDiagramDefinitio
|
||||
|
||||
export const addDetector = (key: string, detector: DiagramDetector, loader?: DiagramLoader) => {
|
||||
if (detectors[key]) {
|
||||
log.error(`Detector with key ${key} already exists`);
|
||||
} else {
|
||||
detectors[key] = { detector, loader };
|
||||
log.warn(`Detector with key ${key} already exists. Overwriting.`);
|
||||
}
|
||||
detectors[key] = { detector, loader };
|
||||
log.debug(`Detector with key ${key} added${loader ? ' with loader' : ''}`);
|
||||
};
|
||||
|
||||
|
||||
@@ -20,6 +20,8 @@ import flowchartElk from '../diagrams/flowchart/elk/detector.js';
|
||||
import timeline from '../diagrams/timeline/detector.js';
|
||||
import mindmap from '../diagrams/mindmap/detector.js';
|
||||
import sankey from '../diagrams/sankey/sankeyDetector.js';
|
||||
import { packet } from '../diagrams/packet/detector.js';
|
||||
import block from '../diagrams/block/blockDetector.js';
|
||||
import { registerLazyLoadedDiagrams } from './detectType.js';
|
||||
import { registerDiagram } from './diagramAPI.js';
|
||||
|
||||
@@ -50,7 +52,6 @@ export const addDiagrams = () => {
|
||||
},
|
||||
},
|
||||
parser: {
|
||||
parser: { yy: {} },
|
||||
parse: () => {
|
||||
throw new Error(
|
||||
'Diagrams beginning with --- are not valid. ' +
|
||||
@@ -87,6 +88,8 @@ export const addDiagrams = () => {
|
||||
journey,
|
||||
quadrantChart,
|
||||
sankey,
|
||||
xychart
|
||||
packet,
|
||||
xychart,
|
||||
block
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,12 +2,12 @@ import { detectType } from './detectType.js';
|
||||
import { getDiagram, registerDiagram } from './diagramAPI.js';
|
||||
import { addDiagrams } from './diagram-orchestration.js';
|
||||
import type { DiagramDetector } from './types.js';
|
||||
import { getDiagramFromText } from '../Diagram.js';
|
||||
import { Diagram } from '../Diagram.js';
|
||||
import { it, describe, expect, beforeAll } from 'vitest';
|
||||
|
||||
addDiagrams();
|
||||
beforeAll(async () => {
|
||||
await getDiagramFromText('sequenceDiagram');
|
||||
await Diagram.fromText('sequenceDiagram');
|
||||
});
|
||||
|
||||
describe('DiagramAPI', () => {
|
||||
@@ -39,7 +39,6 @@ describe('DiagramAPI', () => {
|
||||
parse: (_text) => {
|
||||
return;
|
||||
},
|
||||
parser: { yy: {} },
|
||||
},
|
||||
renderer: {
|
||||
draw: () => {
|
||||
|
||||
@@ -49,7 +49,7 @@ export const registerDiagram = (
|
||||
detector?: DiagramDetector
|
||||
) => {
|
||||
if (diagrams[id]) {
|
||||
throw new Error(`Diagram ${id} already registered.`);
|
||||
log.warn(`Diagram with id ${id} already registered. Overwriting.`);
|
||||
}
|
||||
diagrams[id] = diagram;
|
||||
if (detector) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { MermaidConfig } from '../config.type.js';
|
||||
import type { GanttDiagramConfig, 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';
|
||||
@@ -6,7 +6,7 @@ import * as yaml from 'js-yaml';
|
||||
interface FrontMatterMetadata {
|
||||
title?: string;
|
||||
// Allows custom display modes. Currently used for compact mode in gantt charts.
|
||||
displayMode?: string;
|
||||
displayMode?: GanttDiagramConfig['displayMode'];
|
||||
config?: MermaidConfig;
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ export function extractFrontMatter(text: string): FrontMatterResult {
|
||||
|
||||
// Only add properties that are explicitly supported, if they exist
|
||||
if (parsed.displayMode) {
|
||||
metadata.displayMode = parsed.displayMode.toString();
|
||||
metadata.displayMode = parsed.displayMode.toString() as GanttDiagramConfig['displayMode'];
|
||||
}
|
||||
if (parsed.title) {
|
||||
metadata.title = parsed.title.toString();
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import type * as d3 from 'd3';
|
||||
import type { SetRequired } from 'type-fest';
|
||||
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;
|
||||
@@ -32,13 +33,29 @@ export interface DiagramDB {
|
||||
getDiagramTitle?: () => string;
|
||||
setAccTitle?: (title: string) => void;
|
||||
getAccTitle?: () => string;
|
||||
setAccDescription?: (describetion: string) => void;
|
||||
setAccDescription?: (description: string) => void;
|
||||
getAccDescription?: () => string;
|
||||
|
||||
setDisplayMode?: (title: string) => void;
|
||||
bindFunctions?: (element: Element) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* DiagramDB with fields that is required for all new diagrams.
|
||||
*/
|
||||
export type DiagramDBBase<T extends BaseDiagramConfig> = {
|
||||
getConfig: () => Required<T>;
|
||||
} & SetRequired<
|
||||
DiagramDB,
|
||||
| 'clear'
|
||||
| 'getAccTitle'
|
||||
| 'getDiagramTitle'
|
||||
| 'getAccDescription'
|
||||
| 'setAccDescription'
|
||||
| 'setAccTitle'
|
||||
| 'setDiagramTitle'
|
||||
>;
|
||||
|
||||
// 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.
|
||||
@@ -104,8 +121,8 @@ export type DrawDefinition = (
|
||||
) => void | Promise<void>;
|
||||
|
||||
export interface ParserDefinition {
|
||||
parse: (text: string) => void;
|
||||
parser: { yy: DiagramDB };
|
||||
parse: (text: string) => void | Promise<void>;
|
||||
parser?: { yy: DiagramDB };
|
||||
}
|
||||
|
||||
export type HTML = d3.Selection<HTMLIFrameElement, unknown, Element | null, unknown>;
|
||||
|
||||
@@ -1,16 +1,39 @@
|
||||
import { describe, test, expect } from 'vitest';
|
||||
import { Diagram, getDiagramFromText } from './Diagram.js';
|
||||
import { Diagram } from './Diagram.js';
|
||||
import { addDetector } from './diagram-api/detectType.js';
|
||||
import { addDiagrams } from './diagram-api/diagram-orchestration.js';
|
||||
import type { DiagramLoader } from './diagram-api/types.js';
|
||||
|
||||
addDiagrams();
|
||||
|
||||
const getDummyDiagram = (id: string, title?: string): Awaited<ReturnType<DiagramLoader>> => {
|
||||
return {
|
||||
id,
|
||||
diagram: {
|
||||
db: {
|
||||
getDiagramTitle: () => title ?? id,
|
||||
},
|
||||
parser: {
|
||||
parse: () => {
|
||||
// no-op
|
||||
},
|
||||
},
|
||||
renderer: {
|
||||
draw: () => {
|
||||
// no-op
|
||||
},
|
||||
},
|
||||
styles: {},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
describe('diagram detection', () => {
|
||||
test('should detect inbuilt diagrams', async () => {
|
||||
const graph = (await getDiagramFromText('graph TD; A-->B')) as Diagram;
|
||||
const graph = (await Diagram.fromText('graph TD; A-->B')) as Diagram;
|
||||
expect(graph).toBeInstanceOf(Diagram);
|
||||
expect(graph.type).toBe('flowchart-v2');
|
||||
const sequence = (await getDiagramFromText(
|
||||
const sequence = (await Diagram.fromText(
|
||||
'sequenceDiagram; Alice->>+John: Hello John, how are you?'
|
||||
)) as Diagram;
|
||||
expect(sequence).toBeInstanceOf(Diagram);
|
||||
@@ -21,41 +44,33 @@ describe('diagram detection', () => {
|
||||
addDetector(
|
||||
'loki',
|
||||
(str) => str.startsWith('loki'),
|
||||
() =>
|
||||
Promise.resolve({
|
||||
id: 'loki',
|
||||
diagram: {
|
||||
db: {},
|
||||
parser: {
|
||||
parse: () => {
|
||||
// no-op
|
||||
},
|
||||
parser: {
|
||||
yy: {},
|
||||
},
|
||||
},
|
||||
renderer: {
|
||||
draw: () => {
|
||||
// no-op
|
||||
},
|
||||
},
|
||||
styles: {},
|
||||
},
|
||||
})
|
||||
() => Promise.resolve(getDummyDiagram('loki'))
|
||||
);
|
||||
const diagram = (await getDiagramFromText('loki TD; A-->B')) as Diagram;
|
||||
const diagram = await Diagram.fromText('loki TD; A-->B');
|
||||
expect(diagram).toBeInstanceOf(Diagram);
|
||||
expect(diagram.type).toBe('loki');
|
||||
});
|
||||
|
||||
test('should allow external diagrams to override internal ones with same ID', async () => {
|
||||
const title = 'overridden';
|
||||
addDetector(
|
||||
'flowchart-elk',
|
||||
(str) => str.startsWith('flowchart-elk'),
|
||||
() => Promise.resolve(getDummyDiagram('flowchart-elk', title))
|
||||
);
|
||||
const diagram = await Diagram.fromText('flowchart-elk TD; A-->B');
|
||||
expect(diagram).toBeInstanceOf(Diagram);
|
||||
expect(diagram.db.getDiagramTitle?.()).toBe(title);
|
||||
});
|
||||
|
||||
test('should throw the right error for incorrect diagram', async () => {
|
||||
await expect(getDiagramFromText('graph TD; A-->')).rejects.toThrowErrorMatchingInlineSnapshot(`
|
||||
await expect(Diagram.fromText('graph TD; A-->')).rejects.toThrowErrorMatchingInlineSnapshot(`
|
||||
"Parse error on line 2:
|
||||
graph TD; A-->
|
||||
--------------^
|
||||
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
|
||||
await expect(Diagram.fromText('sequenceDiagram; A-->B')).rejects
|
||||
.toThrowErrorMatchingInlineSnapshot(`
|
||||
"Parse error on line 1:
|
||||
...quenceDiagram; A-->B
|
||||
@@ -65,13 +80,13 @@ Expecting 'TXT', got 'NEWLINE'"
|
||||
});
|
||||
|
||||
test('should throw the right error for unregistered diagrams', async () => {
|
||||
await expect(getDiagramFromText('thor TD; A-->B')).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
await expect(Diagram.fromText('thor TD; A-->B')).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
'"No diagram type detected matching given configuration for text: thor TD; A-->B"'
|
||||
);
|
||||
});
|
||||
|
||||
test('should consider entity codes when present in diagram defination', async () => {
|
||||
const diagram = await getDiagramFromText(`sequenceDiagram
|
||||
const diagram = await Diagram.fromText(`sequenceDiagram
|
||||
A->>B: I #9829; you!
|
||||
B->>A: I #9829; you #infin; times more!`);
|
||||
// @ts-ignore: we need to add types for sequenceDb which will be done in separate PR
|
||||
|
||||
311
packages/mermaid/src/diagrams/block/blockDB.ts
Normal file
311
packages/mermaid/src/diagrams/block/blockDB.ts
Normal file
@@ -0,0 +1,311 @@
|
||||
import clone from 'lodash-es/clone.js';
|
||||
import * as configApi from '../../config.js';
|
||||
import type { DiagramDB } from '../../diagram-api/types.js';
|
||||
import { log } from '../../logger.js';
|
||||
import { clear as commonClear } from '../common/commonDb.js';
|
||||
import type { Block, ClassDef } from './blockTypes.js';
|
||||
|
||||
// Initialize the node database for simple lookups
|
||||
let blockDatabase: Record<string, Block> = {};
|
||||
let edgeList: Block[] = [];
|
||||
let edgeCount: Record<string, number> = {};
|
||||
|
||||
const COLOR_KEYWORD = 'color';
|
||||
const FILL_KEYWORD = 'fill';
|
||||
const BG_FILL = 'bgFill';
|
||||
const STYLECLASS_SEP = ',';
|
||||
|
||||
let classes = {} as Record<string, ClassDef>;
|
||||
|
||||
/**
|
||||
* Called when the parser comes across a (style) class definition
|
||||
* @example classDef my-style fill:#f96;
|
||||
*
|
||||
* @param id - the id of this (style) class
|
||||
* @param styleAttributes - the string with 1 or more style attributes (each separated by a comma)
|
||||
*/
|
||||
export const addStyleClass = function (id: string, styleAttributes = '') {
|
||||
// create a new style class object with this id
|
||||
if (classes[id] === undefined) {
|
||||
classes[id] = { id: id, styles: [], textStyles: [] }; // This is a classDef
|
||||
}
|
||||
const foundClass = classes[id];
|
||||
if (styleAttributes !== undefined && styleAttributes !== null) {
|
||||
styleAttributes.split(STYLECLASS_SEP).forEach((attrib) => {
|
||||
// remove any trailing ;
|
||||
const fixedAttrib = attrib.replace(/([^;]*);/, '$1').trim();
|
||||
|
||||
// replace some style keywords
|
||||
if (attrib.match(COLOR_KEYWORD)) {
|
||||
const newStyle1 = fixedAttrib.replace(FILL_KEYWORD, BG_FILL);
|
||||
const newStyle2 = newStyle1.replace(COLOR_KEYWORD, FILL_KEYWORD);
|
||||
foundClass.textStyles.push(newStyle2);
|
||||
}
|
||||
foundClass.styles.push(fixedAttrib);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Called when the parser comes across a style definition
|
||||
* @example style my-block-id fill:#f96;
|
||||
*
|
||||
* @param id - the id of the block to style
|
||||
* @param styles - the string with 1 or more style attributes (each separated by a comma)
|
||||
*/
|
||||
export const addStyle2Node = function (id: string, styles = '') {
|
||||
const foundBlock = blockDatabase[id];
|
||||
if (styles !== undefined && styles !== null) {
|
||||
foundBlock.styles = styles.split(STYLECLASS_SEP);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Add a CSS/style class to the block with the given id.
|
||||
* If the block isn't already in the list of known blocks, add it.
|
||||
* Might be called by parser when a CSS/style class should be applied to a block
|
||||
*
|
||||
* @param itemIds - The id or a list of ids of the item(s) to apply the css class to
|
||||
* @param cssClassName - CSS class name
|
||||
*/
|
||||
export const setCssClass = function (itemIds: string, cssClassName: string) {
|
||||
itemIds.split(',').forEach(function (id: string) {
|
||||
let foundBlock = blockDatabase[id];
|
||||
if (foundBlock === undefined) {
|
||||
const trimmedId = id.trim();
|
||||
blockDatabase[trimmedId] = { id: trimmedId, type: 'na', children: [] } as Block;
|
||||
foundBlock = blockDatabase[trimmedId];
|
||||
}
|
||||
if (!foundBlock.classes) {
|
||||
foundBlock.classes = [];
|
||||
}
|
||||
foundBlock.classes.push(cssClassName);
|
||||
});
|
||||
};
|
||||
|
||||
const populateBlockDatabase = (_blockList: Block[] | Block[][], parent: Block): void => {
|
||||
const blockList = _blockList.flat();
|
||||
const children = [];
|
||||
for (const block of blockList) {
|
||||
if (block.type === 'classDef') {
|
||||
addStyleClass(block.id, block.css);
|
||||
continue;
|
||||
}
|
||||
if (block.type === 'applyClass') {
|
||||
setCssClass(block.id, block?.styleClass || '');
|
||||
continue;
|
||||
}
|
||||
if (block.type === 'applyStyles') {
|
||||
if (block?.stylesStr) {
|
||||
addStyle2Node(block.id, block?.stylesStr);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (block.type === 'column-setting') {
|
||||
parent.columns = block.columns || -1;
|
||||
} else if (block.type === 'edge') {
|
||||
if (edgeCount[block.id]) {
|
||||
edgeCount[block.id]++;
|
||||
} else {
|
||||
edgeCount[block.id] = 1;
|
||||
}
|
||||
block.id = edgeCount[block.id] + '-' + block.id;
|
||||
edgeList.push(block);
|
||||
} else {
|
||||
if (!block.label) {
|
||||
if (block.type === 'composite') {
|
||||
block.label = '';
|
||||
// log.debug('abc89 composite', block);
|
||||
} else {
|
||||
block.label = block.id;
|
||||
}
|
||||
}
|
||||
const newBlock = !blockDatabase[block.id];
|
||||
if (newBlock) {
|
||||
blockDatabase[block.id] = block;
|
||||
} else {
|
||||
// Add newer relevant data to aggregated node
|
||||
if (block.type !== 'na') {
|
||||
blockDatabase[block.id].type = block.type;
|
||||
}
|
||||
if (block.label !== block.id) {
|
||||
blockDatabase[block.id].label = block.label;
|
||||
}
|
||||
}
|
||||
|
||||
if (block.children) {
|
||||
populateBlockDatabase(block.children, block);
|
||||
}
|
||||
if (block.type === 'space') {
|
||||
// log.debug('abc95 space', block);
|
||||
const w = block.width || 1;
|
||||
for (let j = 0; j < w; j++) {
|
||||
const newBlock = clone(block);
|
||||
newBlock.id = newBlock.id + '-' + j;
|
||||
blockDatabase[newBlock.id] = newBlock;
|
||||
children.push(newBlock);
|
||||
}
|
||||
} else if (newBlock) {
|
||||
children.push(block);
|
||||
}
|
||||
}
|
||||
}
|
||||
parent.children = children;
|
||||
};
|
||||
|
||||
let blocks: Block[] = [];
|
||||
let rootBlock = { id: 'root', type: 'composite', children: [], columns: -1 } as Block;
|
||||
|
||||
const clear = (): void => {
|
||||
log.debug('Clear called');
|
||||
commonClear();
|
||||
rootBlock = { id: 'root', type: 'composite', children: [], columns: -1 } as Block;
|
||||
blockDatabase = { root: rootBlock };
|
||||
blocks = [] as Block[];
|
||||
classes = {} as Record<string, ClassDef>;
|
||||
|
||||
edgeList = [];
|
||||
edgeCount = {};
|
||||
};
|
||||
|
||||
export function typeStr2Type(typeStr: string) {
|
||||
log.debug('typeStr2Type', typeStr);
|
||||
switch (typeStr) {
|
||||
case '[]':
|
||||
return 'square';
|
||||
case '()':
|
||||
log.debug('we have a round');
|
||||
return 'round';
|
||||
case '(())':
|
||||
return 'circle';
|
||||
case '>]':
|
||||
return 'rect_left_inv_arrow';
|
||||
case '{}':
|
||||
return 'diamond';
|
||||
case '{{}}':
|
||||
return 'hexagon';
|
||||
case '([])':
|
||||
return 'stadium';
|
||||
case '[[]]':
|
||||
return 'subroutine';
|
||||
case '[()]':
|
||||
return 'cylinder';
|
||||
case '((()))':
|
||||
return 'doublecircle';
|
||||
case '[//]':
|
||||
return 'lean_right';
|
||||
case '[\\\\]':
|
||||
return 'lean_left';
|
||||
case '[/\\]':
|
||||
return 'trapezoid';
|
||||
case '[\\/]':
|
||||
return 'inv_trapezoid';
|
||||
case '<[]>':
|
||||
return 'block_arrow';
|
||||
default:
|
||||
return 'na';
|
||||
}
|
||||
}
|
||||
|
||||
export function edgeTypeStr2Type(typeStr: string): string {
|
||||
log.debug('typeStr2Type', typeStr);
|
||||
switch (typeStr) {
|
||||
case '==':
|
||||
return 'thick';
|
||||
default:
|
||||
return 'normal';
|
||||
}
|
||||
}
|
||||
|
||||
export function edgeStrToEdgeData(typeStr: string): string {
|
||||
switch (typeStr.trim()) {
|
||||
case '--x':
|
||||
return 'arrow_cross';
|
||||
case '--o':
|
||||
return 'arrow_circle';
|
||||
default:
|
||||
return 'arrow_point';
|
||||
}
|
||||
}
|
||||
|
||||
let cnt = 0;
|
||||
export const generateId = () => {
|
||||
cnt++;
|
||||
return 'id-' + Math.random().toString(36).substr(2, 12) + '-' + cnt;
|
||||
};
|
||||
|
||||
const setHierarchy = (block: Block[]): void => {
|
||||
rootBlock.children = block;
|
||||
populateBlockDatabase(block, rootBlock);
|
||||
blocks = rootBlock.children;
|
||||
};
|
||||
|
||||
const getColumns = (blockId: string): number => {
|
||||
const block = blockDatabase[blockId];
|
||||
if (!block) {
|
||||
return -1;
|
||||
}
|
||||
if (block.columns) {
|
||||
return block.columns;
|
||||
}
|
||||
if (!block.children) {
|
||||
return -1;
|
||||
}
|
||||
return block.children.length;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns all the blocks as a flat array
|
||||
* @returns
|
||||
*/
|
||||
const getBlocksFlat = () => {
|
||||
return [...Object.values(blockDatabase)];
|
||||
};
|
||||
/**
|
||||
* Returns the the hierarchy of blocks
|
||||
* @returns
|
||||
*/
|
||||
const getBlocks = () => {
|
||||
return blocks || [];
|
||||
};
|
||||
|
||||
const getEdges = () => {
|
||||
return edgeList;
|
||||
};
|
||||
const getBlock = (id: string) => {
|
||||
return blockDatabase[id];
|
||||
};
|
||||
|
||||
const setBlock = (block: Block) => {
|
||||
blockDatabase[block.id] = block;
|
||||
};
|
||||
|
||||
const getLogger = () => console;
|
||||
|
||||
/**
|
||||
* Return all of the style classes
|
||||
*/
|
||||
export const getClasses = function () {
|
||||
return classes;
|
||||
};
|
||||
|
||||
const db = {
|
||||
getConfig: () => configApi.getConfig().block,
|
||||
typeStr2Type: typeStr2Type,
|
||||
edgeTypeStr2Type: edgeTypeStr2Type,
|
||||
edgeStrToEdgeData,
|
||||
getLogger,
|
||||
getBlocksFlat,
|
||||
getBlocks,
|
||||
getEdges,
|
||||
setHierarchy,
|
||||
getBlock,
|
||||
setBlock,
|
||||
getColumns,
|
||||
getClasses,
|
||||
clear,
|
||||
generateId,
|
||||
} as const;
|
||||
|
||||
export type BlockDB = typeof db & DiagramDB;
|
||||
export default db;
|
||||
20
packages/mermaid/src/diagrams/block/blockDetector.ts
Normal file
20
packages/mermaid/src/diagrams/block/blockDetector.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import type { DiagramDetector, ExternalDiagramDefinition } from '../../diagram-api/types.js';
|
||||
|
||||
const id = 'block';
|
||||
|
||||
const detector: DiagramDetector = (txt) => {
|
||||
return /^\s*block-beta/.test(txt);
|
||||
};
|
||||
|
||||
const loader = async () => {
|
||||
const { diagram } = await import('./blockDiagram.js');
|
||||
return { id, diagram };
|
||||
};
|
||||
|
||||
const plugin: ExternalDiagramDefinition = {
|
||||
id,
|
||||
detector,
|
||||
loader,
|
||||
};
|
||||
|
||||
export default plugin;
|
||||
13
packages/mermaid/src/diagrams/block/blockDiagram.ts
Normal file
13
packages/mermaid/src/diagrams/block/blockDiagram.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import type { DiagramDefinition } from '../../diagram-api/types.js';
|
||||
// @ts-ignore: jison doesn't export types
|
||||
import parser from './parser/block.jison';
|
||||
import db from './blockDB.js';
|
||||
import flowStyles from './styles.js';
|
||||
import renderer from './blockRenderer.js';
|
||||
|
||||
export const diagram: DiagramDefinition = {
|
||||
parser,
|
||||
db,
|
||||
renderer,
|
||||
styles: flowStyles,
|
||||
};
|
||||
94
packages/mermaid/src/diagrams/block/blockRenderer.ts
Normal file
94
packages/mermaid/src/diagrams/block/blockRenderer.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import {
|
||||
scaleOrdinal as d3scaleOrdinal,
|
||||
schemeTableau10 as d3schemeTableau10,
|
||||
select as d3select,
|
||||
} from 'd3';
|
||||
import type { Diagram } from '../../Diagram.js';
|
||||
import * as configApi from '../../config.js';
|
||||
import type { MermaidConfig } from '../../config.type.js';
|
||||
import insertMarkers from '../../dagre-wrapper/markers.js';
|
||||
import { log } from '../../logger.js';
|
||||
import { configureSvgSize } from '../../setupGraphViewbox.js';
|
||||
import type { BlockDB } from './blockDB.js';
|
||||
import { layout } from './layout.js';
|
||||
import { calculateBlockSizes, insertBlocks, insertEdges } from './renderHelpers.js';
|
||||
|
||||
/**
|
||||
* Returns the all the styles from classDef statements in the graph definition.
|
||||
*
|
||||
* @param text - The text with the classes
|
||||
* @param diagObj - The diagram object
|
||||
* @returns ClassDef - The styles
|
||||
*/
|
||||
export const getClasses = function (text: any, diagObj: any) {
|
||||
return diagObj.db.getClasses();
|
||||
};
|
||||
|
||||
export const draw = async function (
|
||||
text: string,
|
||||
id: string,
|
||||
_version: string,
|
||||
diagObj: Diagram
|
||||
): Promise<void> {
|
||||
const { securityLevel, block: conf } = configApi.getConfig();
|
||||
const db = diagObj.db as BlockDB;
|
||||
let sandboxElement: any;
|
||||
if (securityLevel === 'sandbox') {
|
||||
sandboxElement = d3select('#i' + id);
|
||||
}
|
||||
const root =
|
||||
securityLevel === 'sandbox'
|
||||
? d3select<HTMLBodyElement, unknown>(sandboxElement.nodes()[0].contentDocument.body)
|
||||
: d3select<HTMLBodyElement, unknown>('body');
|
||||
|
||||
const svg =
|
||||
securityLevel === 'sandbox'
|
||||
? root.select<SVGSVGElement>(`[id="${id}"]`)
|
||||
: d3select<SVGSVGElement, unknown>(`[id="${id}"]`);
|
||||
|
||||
// Define the supported markers for the diagram
|
||||
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, true);
|
||||
insertMarkers(svg, markers, diagObj.type, id);
|
||||
|
||||
const bl = db.getBlocks();
|
||||
const blArr = db.getBlocksFlat();
|
||||
const edges = db.getEdges();
|
||||
|
||||
const nodes = svg.insert('g').attr('class', 'block');
|
||||
await calculateBlockSizes(nodes, bl, db);
|
||||
const bounds = layout(db);
|
||||
await insertBlocks(nodes, bl, db);
|
||||
await insertEdges(nodes, edges, blArr, db, id);
|
||||
|
||||
// log.debug('Here', bl);
|
||||
|
||||
// Establish svg dimensions and get width and height
|
||||
//
|
||||
// const bounds2 = nodes.node().getBoundingClientRect();
|
||||
// Why, oh why ????
|
||||
if (bounds) {
|
||||
const bounds2 = bounds;
|
||||
const magicFactor = Math.max(1, Math.round(0.125 * (bounds2.width / bounds2.height)));
|
||||
const height = bounds2.height + magicFactor + 10;
|
||||
const width = bounds2.width + 10;
|
||||
const { useMaxWidth } = conf as Exclude<MermaidConfig['block'], undefined>;
|
||||
configureSvgSize(svg, height, width, !!useMaxWidth);
|
||||
log.debug('Here Bounds', bounds, bounds2);
|
||||
svg.attr(
|
||||
'viewBox',
|
||||
`${bounds2.x - 5} ${bounds2.y - 5} ${bounds2.width + 10} ${bounds2.height + 10}`
|
||||
);
|
||||
}
|
||||
|
||||
// Get color scheme for the graph
|
||||
const colorScheme = d3scaleOrdinal(d3schemeTableau10);
|
||||
};
|
||||
|
||||
export default {
|
||||
draw,
|
||||
getClasses,
|
||||
};
|
||||
68
packages/mermaid/src/diagrams/block/blockTypes.ts
Normal file
68
packages/mermaid/src/diagrams/block/blockTypes.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
export type { BlockDiagramConfig as BlockConfig } from '../../config.type.js';
|
||||
|
||||
export type BlockType =
|
||||
| 'na'
|
||||
| 'column-setting'
|
||||
| 'edge'
|
||||
| 'round'
|
||||
| 'block_arrow'
|
||||
| 'space'
|
||||
| 'square'
|
||||
| 'diamond'
|
||||
| 'hexagon'
|
||||
| 'odd'
|
||||
| 'lean_right'
|
||||
| 'lean_left'
|
||||
| 'trapezoid'
|
||||
| 'inv_trapezoid'
|
||||
| 'rect_left_inv_arrow'
|
||||
| 'odd_right'
|
||||
| 'circle'
|
||||
| 'ellipse'
|
||||
| 'stadium'
|
||||
| 'subroutine'
|
||||
| 'cylinder'
|
||||
| 'group'
|
||||
| 'doublecircle'
|
||||
| 'classDef'
|
||||
| 'applyClass'
|
||||
| 'applyStyles'
|
||||
| 'composite';
|
||||
|
||||
export interface Block {
|
||||
// When the block have the type edge, the start and end are the id of the source and target blocks
|
||||
start?: string;
|
||||
end?: string;
|
||||
arrowTypeEnd?: string;
|
||||
arrowTypeStart?: string;
|
||||
width?: number;
|
||||
id: string;
|
||||
label?: string;
|
||||
intersect?: any;
|
||||
parent?: Block;
|
||||
type?: BlockType;
|
||||
children: Block[];
|
||||
size?: {
|
||||
width: number;
|
||||
height: number;
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
node?: any;
|
||||
columns?: number; // | TBlockColumnsDefaultValue;
|
||||
classes?: string[];
|
||||
directions?: string[];
|
||||
css?: string;
|
||||
styleClass?: string;
|
||||
styles?: string[];
|
||||
stylesStr?: string;
|
||||
widthInColumns?: number;
|
||||
}
|
||||
|
||||
export interface ClassDef {
|
||||
id: string;
|
||||
textStyles: string[];
|
||||
styles: string[];
|
||||
}
|
||||
|
||||
export type Direction = 'up' | 'down' | 'left' | 'right' | 'x' | 'y';
|
||||
8
packages/mermaid/src/diagrams/block/blockUtils.ts
Normal file
8
packages/mermaid/src/diagrams/block/blockUtils.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export const prepareTextForParsing = (text: string): string => {
|
||||
const textToParse = text
|
||||
.replaceAll(/^[^\S\n\r]+|[^\S\n\r]+$/g, '') // remove all trailing spaces for each row
|
||||
.replaceAll(/([\n\r])+/g, '\n') // remove empty lines duplicated
|
||||
.trim();
|
||||
|
||||
return textToParse;
|
||||
};
|
||||
12
packages/mermaid/src/diagrams/block/layout.spec.ts
Normal file
12
packages/mermaid/src/diagrams/block/layout.spec.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { calculateBlockPosition } from './layout.js';
|
||||
|
||||
describe('Layout', function () {
|
||||
it('should calculate position correctly', () => {
|
||||
expect(calculateBlockPosition(2, 0)).toEqual({ px: 0, py: 0 });
|
||||
expect(calculateBlockPosition(2, 1)).toEqual({ px: 1, py: 0 });
|
||||
expect(calculateBlockPosition(2, 2)).toEqual({ px: 0, py: 1 });
|
||||
expect(calculateBlockPosition(2, 3)).toEqual({ px: 1, py: 1 });
|
||||
expect(calculateBlockPosition(2, 4)).toEqual({ px: 0, py: 2 });
|
||||
expect(calculateBlockPosition(1, 3)).toEqual({ px: 0, py: 3 });
|
||||
});
|
||||
});
|
||||
329
packages/mermaid/src/diagrams/block/layout.ts
Normal file
329
packages/mermaid/src/diagrams/block/layout.ts
Normal file
@@ -0,0 +1,329 @@
|
||||
import type { BlockDB } from './blockDB.js';
|
||||
import type { Block } from './blockTypes.js';
|
||||
import { log } from '../../logger.js';
|
||||
import { getConfig } from '../../diagram-api/diagramAPI.js';
|
||||
const padding = getConfig()?.block?.padding || 8;
|
||||
|
||||
interface BlockPosition {
|
||||
px: number;
|
||||
py: number;
|
||||
}
|
||||
|
||||
export function calculateBlockPosition(columns: number, position: number): BlockPosition {
|
||||
// log.debug('calculateBlockPosition abc89', columns, position);
|
||||
// Ensure that columns is a positive integer
|
||||
if (columns === 0 || !Number.isInteger(columns)) {
|
||||
throw new Error('Columns must be an integer !== 0.');
|
||||
}
|
||||
|
||||
// Ensure that position is a non-negative integer
|
||||
if (position < 0 || !Number.isInteger(position)) {
|
||||
throw new Error('Position must be a non-negative integer.' + position);
|
||||
}
|
||||
|
||||
if (columns < 0) {
|
||||
// Auto columns is set
|
||||
return { px: position, py: 0 };
|
||||
}
|
||||
if (columns === 1) {
|
||||
// Auto columns is set
|
||||
return { px: 0, py: position };
|
||||
}
|
||||
// Calculate posX and posY
|
||||
const px = position % columns;
|
||||
const py = Math.floor(position / columns);
|
||||
// log.debug('calculateBlockPosition abc89', columns, position, '=> (', px, py, ')');
|
||||
return { px, py };
|
||||
}
|
||||
|
||||
const getMaxChildSize = (block: Block) => {
|
||||
let maxWidth = 0;
|
||||
let maxHeight = 0;
|
||||
// find max width of children
|
||||
// log.debug('getMaxChildSize abc95 (start) parent:', block.id);
|
||||
for (const child of block.children) {
|
||||
const { width, height, x, y } = child.size || { width: 0, height: 0, x: 0, y: 0 };
|
||||
log.debug(
|
||||
'getMaxChildSize abc95 child:',
|
||||
child.id,
|
||||
'width:',
|
||||
width,
|
||||
'height:',
|
||||
height,
|
||||
'x:',
|
||||
x,
|
||||
'y:',
|
||||
y,
|
||||
child.type
|
||||
);
|
||||
if (child.type === 'space') {
|
||||
continue;
|
||||
}
|
||||
if (width > maxWidth) {
|
||||
maxWidth = width / (block.widthInColumns || 1);
|
||||
}
|
||||
if (height > maxHeight) {
|
||||
maxHeight = height;
|
||||
}
|
||||
}
|
||||
return { width: maxWidth, height: maxHeight };
|
||||
};
|
||||
|
||||
function setBlockSizes(block: Block, db: BlockDB, siblingWidth = 0, siblingHeight = 0) {
|
||||
log.debug(
|
||||
'setBlockSizes abc95 (start)',
|
||||
block.id,
|
||||
block?.size?.x,
|
||||
'block width =',
|
||||
block?.size,
|
||||
'sieblingWidth',
|
||||
siblingWidth
|
||||
);
|
||||
if (!block?.size?.width) {
|
||||
block.size = {
|
||||
width: siblingWidth,
|
||||
height: siblingHeight,
|
||||
x: 0,
|
||||
y: 0,
|
||||
};
|
||||
}
|
||||
let maxWidth = 0;
|
||||
let maxHeight = 0;
|
||||
|
||||
if (block.children?.length > 0) {
|
||||
for (const child of block.children) {
|
||||
setBlockSizes(child, db);
|
||||
}
|
||||
// find max width of children
|
||||
const childSize = getMaxChildSize(block);
|
||||
maxWidth = childSize.width;
|
||||
maxHeight = childSize.height;
|
||||
log.debug('setBlockSizes abc95 maxWidth of', block.id, ':s children is ', maxWidth, maxHeight);
|
||||
|
||||
// set width of block to max width of children
|
||||
for (const child of block.children) {
|
||||
if (child.size) {
|
||||
log.debug(
|
||||
`abc95 Setting size of children of ${block.id} id=${child.id} ${maxWidth} ${maxHeight} ${child.size}`
|
||||
);
|
||||
child.size.width =
|
||||
maxWidth * (child.widthInColumns || 1) + padding * ((child.widthInColumns || 1) - 1);
|
||||
child.size.height = maxHeight;
|
||||
child.size.x = 0;
|
||||
child.size.y = 0;
|
||||
|
||||
log.debug(
|
||||
`abc95 updating size of ${block.id} children child:${child.id} maxWidth:${maxWidth} maxHeight:${maxHeight}`
|
||||
);
|
||||
}
|
||||
}
|
||||
for (const child of block.children) {
|
||||
setBlockSizes(child, db, maxWidth, maxHeight);
|
||||
}
|
||||
|
||||
const columns = block.columns || -1;
|
||||
let numItems = 0;
|
||||
for (const child of block.children) {
|
||||
numItems += child.widthInColumns || 1;
|
||||
}
|
||||
|
||||
// The width and height in number blocks
|
||||
let xSize = block.children.length;
|
||||
if (columns > 0 && columns < numItems) {
|
||||
xSize = columns;
|
||||
}
|
||||
|
||||
const w = block.widthInColumns || 1;
|
||||
|
||||
const ySize = Math.ceil(numItems / xSize);
|
||||
|
||||
let width = xSize * (maxWidth + padding) + padding;
|
||||
let height = ySize * (maxHeight + padding) + padding;
|
||||
// If maxWidth
|
||||
if (width < siblingWidth) {
|
||||
log.debug(
|
||||
`Detected to small siebling: abc95 ${block.id} sieblingWidth ${siblingWidth} sieblingHeight ${siblingHeight} width ${width}`
|
||||
);
|
||||
width = siblingWidth;
|
||||
height = siblingHeight;
|
||||
const childWidth = (siblingWidth - xSize * padding - padding) / xSize;
|
||||
const childHeight = (siblingHeight - ySize * padding - padding) / ySize;
|
||||
// cspell:ignore indata
|
||||
log.debug('Size indata abc88', block.id, 'childWidth', childWidth, 'maxWidth', maxWidth);
|
||||
log.debug('Size indata abc88', block.id, 'childHeight', childHeight, 'maxHeight', maxHeight);
|
||||
log.debug('Size indata abc88 xSize', xSize, 'padding', padding);
|
||||
|
||||
// set width of block to max width of children
|
||||
for (const child of block.children) {
|
||||
if (child.size) {
|
||||
child.size.width = childWidth;
|
||||
child.size.height = childHeight;
|
||||
child.size.x = 0;
|
||||
child.size.y = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.debug(
|
||||
`abc95 (finale calc) ${block.id} xSize ${xSize} ySize ${ySize} columns ${columns}${
|
||||
block.children.length
|
||||
} width=${Math.max(width, block.size?.width || 0)}`
|
||||
);
|
||||
if (width < (block?.size?.width || 0)) {
|
||||
width = block?.size?.width || 0;
|
||||
|
||||
// Grow children to fit
|
||||
const num = columns > 0 ? Math.min(block.children.length, columns) : block.children.length;
|
||||
if (num > 0) {
|
||||
const childWidth = (width - num * padding - padding) / num;
|
||||
log.debug('abc95 (growing to fit) width', block.id, width, block.size?.width, childWidth);
|
||||
for (const child of block.children) {
|
||||
if (child.size) {
|
||||
child.size.width = childWidth;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
block.size = {
|
||||
width,
|
||||
height,
|
||||
x: 0,
|
||||
y: 0,
|
||||
};
|
||||
}
|
||||
|
||||
log.debug(
|
||||
'setBlockSizes abc94 (done)',
|
||||
block.id,
|
||||
block?.size?.x,
|
||||
block?.size?.width,
|
||||
block?.size?.y,
|
||||
block?.size?.height
|
||||
);
|
||||
}
|
||||
|
||||
function layoutBlocks(block: Block, db: BlockDB) {
|
||||
log.debug(
|
||||
`abc85 layout blocks (=>layoutBlocks) ${block.id} x: ${block?.size?.x} y: ${block?.size?.y} width: ${block?.size?.width}`
|
||||
);
|
||||
const columns = block.columns || -1;
|
||||
log.debug('layoutBlocks columns abc95', block.id, '=>', columns, block);
|
||||
if (
|
||||
block.children && // find max width of children
|
||||
block.children.length > 0
|
||||
) {
|
||||
const width = block?.children[0]?.size?.width || 0;
|
||||
const widthOfChildren = block.children.length * width + (block.children.length - 1) * padding;
|
||||
|
||||
log.debug('widthOfChildren 88', widthOfChildren, 'posX');
|
||||
|
||||
// let first = true;
|
||||
let columnPos = 0;
|
||||
log.debug('abc91 block?.size?.x', block.id, block?.size?.x);
|
||||
let startingPosX = block?.size?.x ? block?.size?.x + (-block?.size?.width / 2 || 0) : -padding;
|
||||
let rowPos = 0;
|
||||
for (const child of block.children) {
|
||||
const parent = block;
|
||||
|
||||
if (!child.size) {
|
||||
continue;
|
||||
}
|
||||
const { width, height } = child.size;
|
||||
const { px, py } = calculateBlockPosition(columns, columnPos);
|
||||
if (py != rowPos) {
|
||||
rowPos = py;
|
||||
startingPosX = block?.size?.x ? block?.size?.x + (-block?.size?.width / 2 || 0) : -padding;
|
||||
log.debug('New row in layout for block', block.id, ' and child ', child.id, rowPos);
|
||||
}
|
||||
log.debug(
|
||||
`abc89 layout blocks (child) id: ${child.id} Pos: ${columnPos} (px, py) ${px},${py} (${parent?.size?.x},${parent?.size?.y}) parent: ${parent.id} width: ${width}${padding}`
|
||||
);
|
||||
if (parent.size) {
|
||||
const halfWidth = width / 2;
|
||||
child.size.x = startingPosX + padding + halfWidth;
|
||||
|
||||
// cspell:ignore pyid
|
||||
log.debug(
|
||||
`abc91 layout blocks (calc) px, pyid:${
|
||||
child.id
|
||||
} startingPos=X${startingPosX} new startingPosX${
|
||||
child.size.x
|
||||
} ${halfWidth} padding=${padding} width=${width} halfWidth=${halfWidth} => x:${
|
||||
child.size.x
|
||||
} y:${child.size.y} ${child.widthInColumns} (width * (child?.w || 1)) / 2 ${
|
||||
(width * (child?.widthInColumns || 1)) / 2
|
||||
}`
|
||||
);
|
||||
|
||||
startingPosX = child.size.x + halfWidth;
|
||||
|
||||
child.size.y =
|
||||
parent.size.y - parent.size.height / 2 + py * (height + padding) + height / 2 + padding;
|
||||
|
||||
log.debug(
|
||||
`abc88 layout blocks (calc) px, pyid:${
|
||||
child.id
|
||||
}startingPosX${startingPosX}${padding}${halfWidth}=>x:${child.size.x}y:${child.size.y}${
|
||||
child.widthInColumns
|
||||
}(width * (child?.w || 1)) / 2${(width * (child?.widthInColumns || 1)) / 2}`
|
||||
);
|
||||
}
|
||||
|
||||
// posY += height + padding;
|
||||
if (child.children) {
|
||||
layoutBlocks(child, db);
|
||||
}
|
||||
columnPos += child?.widthInColumns || 1;
|
||||
log.debug('abc88 columnsPos', child, columnPos);
|
||||
}
|
||||
}
|
||||
log.debug(
|
||||
`layout blocks (<==layoutBlocks) ${block.id} x: ${block?.size?.x} y: ${block?.size?.y} width: ${block?.size?.width}`
|
||||
);
|
||||
}
|
||||
|
||||
function findBounds(
|
||||
block: Block,
|
||||
{ minX, minY, maxX, maxY } = { minX: 0, minY: 0, maxX: 0, maxY: 0 }
|
||||
) {
|
||||
if (block.size && block.id !== 'root') {
|
||||
const { x, y, width, height } = block.size;
|
||||
if (x - width / 2 < minX) {
|
||||
minX = x - width / 2;
|
||||
}
|
||||
if (y - height / 2 < minY) {
|
||||
minY = y - height / 2;
|
||||
}
|
||||
if (x + width / 2 > maxX) {
|
||||
maxX = x + width / 2;
|
||||
}
|
||||
if (y + height / 2 > maxY) {
|
||||
maxY = y + height / 2;
|
||||
}
|
||||
}
|
||||
if (block.children) {
|
||||
for (const child of block.children) {
|
||||
({ minX, minY, maxX, maxY } = findBounds(child, { minX, minY, maxX, maxY }));
|
||||
}
|
||||
}
|
||||
return { minX, minY, maxX, maxY };
|
||||
}
|
||||
|
||||
export function layout(db: BlockDB) {
|
||||
const root = db.getBlock('root');
|
||||
if (!root) {
|
||||
return;
|
||||
}
|
||||
|
||||
setBlockSizes(root, db, 0, 0);
|
||||
layoutBlocks(root, db);
|
||||
// Position blocks relative to parents
|
||||
// positionBlock(root, root, db);
|
||||
log.debug('getBlocks', JSON.stringify(root, null, 2));
|
||||
|
||||
const { minX, minY, maxX, maxY } = findBounds(root);
|
||||
|
||||
const height = maxY - minY;
|
||||
const width = maxX - minX;
|
||||
return { x: minX, y: minY, width, height };
|
||||
}
|
||||
290
packages/mermaid/src/diagrams/block/parser/block.jison
Normal file
290
packages/mermaid/src/diagrams/block/parser/block.jison
Normal file
@@ -0,0 +1,290 @@
|
||||
/** mermaid */
|
||||
|
||||
//---------------------------------------------------------
|
||||
// We support csv format as defined here:
|
||||
// https://www.ietf.org/rfc/rfc4180.txt
|
||||
// There are some minor changes for compliance with jison
|
||||
// We also parse only 3 columns: source,target,value
|
||||
// And allow blank lines for visual purposes
|
||||
//---------------------------------------------------------
|
||||
|
||||
%lex
|
||||
%x acc_title
|
||||
%x acc_descr
|
||||
%x acc_descr_multiline
|
||||
%x string
|
||||
%x space
|
||||
%x md_string
|
||||
%x NODE
|
||||
%x BLOCK_ARROW
|
||||
%x ARROW_DIR
|
||||
%x LLABEL
|
||||
%x CLASS
|
||||
%x CLASS_STYLE
|
||||
%x CLASSDEF
|
||||
%x CLASSDEFID
|
||||
%x STYLE_STMNT
|
||||
%x STYLE_DEFINITION
|
||||
|
||||
|
||||
// as per section 6.1 of RFC 2234 [2]
|
||||
COMMA \u002C
|
||||
CR \u000D
|
||||
LF \u000A
|
||||
CRLF \u000D\u000A
|
||||
|
||||
|
||||
%%
|
||||
|
||||
"block-beta" { return 'BLOCK_DIAGRAM_KEY'; }
|
||||
"block"\s+ { yy.getLogger().debug('Found space-block'); return 'block';}
|
||||
"block"\n+ { yy.getLogger().debug('Found nl-block'); return 'block';}
|
||||
"block:" { yy.getLogger().debug('Found space-block'); return 'id-block';}
|
||||
// \s*\%\%.* { yy.getLogger().debug('Found comment',yytext); }
|
||||
[\s]+ { yy.getLogger().debug('.', yytext); /* skip all whitespace */ }
|
||||
[\n]+ {yy.getLogger().debug('_', yytext); /* skip all whitespace */ }
|
||||
// [\n] return 'NL';
|
||||
<INITIAL>({CRLF}|{LF}) { return 'NL' }
|
||||
"columns"\s+"auto" { yytext=-1; return 'COLUMNS'; }
|
||||
"columns"\s+[\d]+ { yytext = yytext.replace(/columns\s+/,''); yy.getLogger().debug('COLUMNS (LEX)', yytext); return 'COLUMNS'; }
|
||||
["][`] { this.pushState("md_string");}
|
||||
<md_string>[^`"]+ { return "MD_STR";}
|
||||
<md_string>[`]["] { this.popState();}
|
||||
["] this.pushState("string");
|
||||
<string>["] { yy.getLogger().debug('LEX: POPPING STR:', yytext);this.popState();}
|
||||
<string>[^"]* { yy.getLogger().debug('LEX: STR end:', yytext); return "STR";}
|
||||
space[:]\d+ { yytext = yytext.replace(/space\:/,'');yy.getLogger().debug('SPACE NUM (LEX)', yytext); return 'SPACE_BLOCK'; }
|
||||
space { yytext = '1'; yy.getLogger().debug('COLUMNS (LEX)', yytext); return 'SPACE_BLOCK'; }
|
||||
"default" return 'DEFAULT';
|
||||
"linkStyle" return 'LINKSTYLE';
|
||||
"interpolate" return 'INTERPOLATE';
|
||||
|
||||
"classDef"\s+ { this.pushState('CLASSDEF'); return 'classDef'; }
|
||||
<CLASSDEF>DEFAULT\s+ { this.popState(); this.pushState('CLASSDEFID'); return 'DEFAULT_CLASSDEF_ID' }
|
||||
<CLASSDEF>\w+\s+ { this.popState(); this.pushState('CLASSDEFID'); return 'CLASSDEF_ID' }
|
||||
<CLASSDEFID>[^\n]* { this.popState(); return 'CLASSDEF_STYLEOPTS' }
|
||||
|
||||
"class"\s+ { this.pushState('CLASS'); return 'class'; }
|
||||
<CLASS>(\w+)+((","\s*\w+)*) { this.popState(); this.pushState('CLASS_STYLE'); return 'CLASSENTITY_IDS' }
|
||||
<CLASS_STYLE>[^\n]* { this.popState(); return 'STYLECLASS' }
|
||||
|
||||
"style"\s+ { this.pushState('STYLE_STMNT'); return 'style'; }
|
||||
<STYLE_STMNT>(\w+)+((","\s*\w+)*) { this.popState(); this.pushState('STYLE_DEFINITION'); return 'STYLE_ENTITY_IDS' }
|
||||
<STYLE_DEFINITION>[^\n]* { this.popState(); return 'STYLE_DEFINITION_DATA' }
|
||||
|
||||
accTitle\s*":"\s* { this.pushState("acc_title");return 'acc_title'; }
|
||||
<acc_title>(?!\n|;|#)*[^\n]* { this.popState(); return "acc_title_value"; }
|
||||
accDescr\s*":"\s* { this.pushState("acc_descr");return 'acc_descr'; }
|
||||
<acc_descr>(?!\n|;|#)*[^\n]* { this.popState(); return "acc_descr_value"; }
|
||||
accDescr\s*"{"\s* { this.pushState("acc_descr_multiline");}
|
||||
<acc_descr_multiline>[\}] { this.popState(); }
|
||||
<acc_descr_multiline>[^\}]* return "acc_descr_multiline_value";
|
||||
"end"\b\s* return 'end';
|
||||
|
||||
// Node end of shape
|
||||
<NODE>"(((" { this.popState();yy.getLogger().debug('Lex: (('); return "NODE_DEND"; }
|
||||
<NODE>")))" { this.popState();yy.getLogger().debug('Lex: (('); return "NODE_DEND"; }
|
||||
<NODE>[\)]\) { this.popState();yy.getLogger().debug('Lex: ))'); return "NODE_DEND"; }
|
||||
<NODE>"}}" { this.popState();yy.getLogger().debug('Lex: (('); return "NODE_DEND"; }
|
||||
<NODE>"}" { this.popState();yy.getLogger().debug('Lex: (('); return "NODE_DEND"; }
|
||||
<NODE>"(-" { this.popState();yy.getLogger().debug('Lex: (-'); return "NODE_DEND"; }
|
||||
<NODE>"-)" { this.popState();yy.getLogger().debug('Lex: -)'); return "NODE_DEND"; }
|
||||
<NODE>"((" { this.popState();yy.getLogger().debug('Lex: (('); return "NODE_DEND"; }
|
||||
<NODE>"]]" { this.popState();yy.getLogger().debug('Lex: ]]'); return "NODE_DEND"; }
|
||||
<NODE>"(" { this.popState();yy.getLogger().debug('Lex: ('); return "NODE_DEND"; }
|
||||
<NODE>"])" { this.popState();yy.getLogger().debug('Lex: ])'); return "NODE_DEND"; }
|
||||
<NODE>"\\]" { this.popState();yy.getLogger().debug('Lex: /]'); return "NODE_DEND"; }
|
||||
<NODE>"/]" { this.popState();yy.getLogger().debug('Lex: /]'); return "NODE_DEND"; }
|
||||
<NODE>")]" { this.popState();yy.getLogger().debug('Lex: )]'); return "NODE_DEND"; }
|
||||
<NODE>[\)] { this.popState();yy.getLogger().debug('Lex: )'); return "NODE_DEND"; }
|
||||
<NODE>\]\> { this.popState();yy.getLogger().debug('Lex: ]>'); return "NODE_DEND"; }
|
||||
<NODE>[\]] { this.popState();yy.getLogger().debug('Lex: ]'); return "NODE_DEND"; }
|
||||
|
||||
// Start of nodes with shapes and description
|
||||
"-)" { yy.getLogger().debug('Lexa: -)'); this.pushState('NODE');return 'NODE_DSTART'; }
|
||||
"(-" { yy.getLogger().debug('Lexa: (-'); this.pushState('NODE');return 'NODE_DSTART'; }
|
||||
"))" { yy.getLogger().debug('Lexa: ))'); this.pushState('NODE');return 'NODE_DSTART'; }
|
||||
")" { yy.getLogger().debug('Lexa: )'); this.pushState('NODE');return 'NODE_DSTART'; }
|
||||
"(((" { yy.getLogger().debug('Lex: ((('); this.pushState('NODE');return 'NODE_DSTART'; }
|
||||
"((" { yy.getLogger().debug('Lexa: )'); this.pushState('NODE');return 'NODE_DSTART'; }
|
||||
"{{" { yy.getLogger().debug('Lexa: )'); this.pushState('NODE');return 'NODE_DSTART'; }
|
||||
"{" { yy.getLogger().debug('Lexa: )'); this.pushState('NODE');return 'NODE_DSTART'; }
|
||||
">" { yy.getLogger().debug('Lexc: >'); this.pushState('NODE');return 'NODE_DSTART'; }
|
||||
"([" { yy.getLogger().debug('Lexa: (['); this.pushState('NODE');return 'NODE_DSTART'; }
|
||||
"(" { yy.getLogger().debug('Lexa: )'); this.pushState('NODE');return 'NODE_DSTART'; }
|
||||
"[[" { this.pushState('NODE');return 'NODE_DSTART'; }
|
||||
"[|" { this.pushState('NODE');return 'NODE_DSTART'; }
|
||||
"[(" { this.pushState('NODE');return 'NODE_DSTART'; }
|
||||
")))" { this.pushState('NODE');return 'NODE_DSTART'; }
|
||||
"[\\" { this.pushState('NODE');return 'NODE_DSTART'; }
|
||||
"[/" { this.pushState('NODE');return 'NODE_DSTART'; }
|
||||
"[\\" { this.pushState('NODE');return 'NODE_DSTART'; }
|
||||
"[" { yy.getLogger().debug('Lexa: ['); this.pushState('NODE');return 'NODE_DSTART'; }
|
||||
|
||||
"<[" { this.pushState('BLOCK_ARROW');yy.getLogger().debug('LEX ARR START');return 'BLOCK_ARROW_START'; }
|
||||
|
||||
[^\(\[\n\-\)\{\}\s\<\>:]+ { yy.getLogger().debug('Lex: NODE_ID', yytext);return 'NODE_ID'; }
|
||||
<<EOF>> { yy.getLogger().debug('Lex: EOF', yytext);return 'EOF'; }
|
||||
|
||||
// Handling of strings in node
|
||||
<BLOCK_ARROW>["][`] { this.pushState("md_string");}
|
||||
<NODE>["][`] { this.pushState("md_string");}
|
||||
<md_string>[^`"]+ { return "NODE_DESCR";}
|
||||
<md_string>[`]["] { this.popState();}
|
||||
<NODE>["] { yy.getLogger().debug('Lex: Starting string');this.pushState("string");}
|
||||
<BLOCK_ARROW>["] { yy.getLogger().debug('LEX ARR: Starting string');this.pushState("string");}
|
||||
<string>[^"]+ { yy.getLogger().debug('LEX: NODE_DESCR:', yytext); return "NODE_DESCR";}
|
||||
<string>["] {yy.getLogger().debug('LEX POPPING');this.popState();}
|
||||
|
||||
<BLOCK_ARROW>"]>"\s*"(" { yy.getLogger().debug('Lex: =>BAE'); this.pushState('ARROW_DIR'); }
|
||||
<ARROW_DIR>","?\s*right\s* { yytext = yytext.replace(/^,\s*/, ''); yy.getLogger().debug('Lex (right): dir:',yytext);return "DIR"; }
|
||||
<ARROW_DIR>","?\s*left\s* { yytext = yytext.replace(/^,\s*/, ''); yy.getLogger().debug('Lex (left):',yytext);return "DIR"; }
|
||||
<ARROW_DIR>","?\s*x\s* { yytext = yytext.replace(/^,\s*/, ''); yy.getLogger().debug('Lex (x):',yytext); return "DIR"; }
|
||||
<ARROW_DIR>","?\s*y\s* { yytext = yytext.replace(/^,\s*/, ''); yy.getLogger().debug('Lex (y):',yytext); return "DIR"; }
|
||||
<ARROW_DIR>","?\s*up\s* { yytext = yytext.replace(/^,\s*/, ''); yy.getLogger().debug('Lex (up):',yytext); return "DIR"; }
|
||||
<ARROW_DIR>","?\s*down\s* { yytext = yytext.replace(/^,\s*/, ''); yy.getLogger().debug('Lex (down):',yytext); return "DIR"; }
|
||||
<ARROW_DIR>")"\s* { yytext=']>';yy.getLogger().debug('Lex (ARROW_DIR end):',yytext);this.popState();this.popState();return "BLOCK_ARROW_END"; }
|
||||
|
||||
// Edges
|
||||
\s*[xo<]?\-\-+[-xo>]\s* { yy.getLogger().debug('Lex: LINK', '#'+yytext+'#'); return 'LINK'; }
|
||||
\s*[xo<]?\=\=+[=xo>]\s* { yy.getLogger().debug('Lex: LINK', yytext); return 'LINK'; }
|
||||
\s*[xo<]?\-?\.+\-[xo>]?\s* { yy.getLogger().debug('Lex: LINK', yytext); return 'LINK'; }
|
||||
\s*\~\~[\~]+\s* { yy.getLogger().debug('Lex: LINK', yytext); return 'LINK'; }
|
||||
\s*[xo<]?\-\-\s* { yy.getLogger().debug('Lex: START_LINK', yytext);this.pushState("LLABEL");return 'START_LINK'; }
|
||||
\s*[xo<]?\=\=\s* { yy.getLogger().debug('Lex: START_LINK', yytext);this.pushState("LLABEL");return 'START_LINK'; }
|
||||
\s*[xo<]?\-\.\s* { yy.getLogger().debug('Lex: START_LINK', yytext);this.pushState("LLABEL");return 'START_LINK'; }
|
||||
<LLABEL>["][`] { this.pushState("md_string");}
|
||||
<LLABEL>["] { yy.getLogger().debug('Lex: Starting string');this.pushState("string"); return "LINK_LABEL";}
|
||||
<LLABEL>\s*[xo<]?\-\-+[-xo>]\s* { this.popState(); yy.getLogger().debug('Lex: LINK', '#'+yytext+'#'); return 'LINK'; }
|
||||
<LLABEL>\s*[xo<]?\=\=+[=xo>]\s* { this.popState(); yy.getLogger().debug('Lex: LINK', yytext); return 'LINK'; }
|
||||
<LLABEL>\s*[xo<]?\-?\.+\-[xo>]?\s* { this.popState(); yy.getLogger().debug('Lex: LINK', yytext); return 'LINK'; }
|
||||
':'\d+ { yy.getLogger().debug('Lex: COLON', yytext); yytext=yytext.slice(1);return 'SIZE'; }
|
||||
|
||||
/lex
|
||||
|
||||
%left '^'
|
||||
%start start
|
||||
|
||||
%% // language grammar
|
||||
|
||||
spaceLines
|
||||
: SPACELINE
|
||||
| spaceLines SPACELINE
|
||||
| spaceLines NL
|
||||
;
|
||||
|
||||
separator
|
||||
: NL
|
||||
{yy.getLogger().debug('Rule: separator (NL) ');}
|
||||
| SPACE
|
||||
{yy.getLogger().debug('Rule: separator (Space) ');}
|
||||
| EOF
|
||||
{yy.getLogger().debug('Rule: separator (EOF) ');}
|
||||
;
|
||||
|
||||
start: BLOCK_DIAGRAM_KEY document EOF
|
||||
{ yy.getLogger().debug("Rule: hierarchy: ", $2); yy.setHierarchy($2); }
|
||||
;
|
||||
|
||||
|
||||
stop
|
||||
: NL {yy.getLogger().debug('Stop NL ');}
|
||||
| EOF {yy.getLogger().debug('Stop EOF ');}
|
||||
// | SPACELINE
|
||||
| stop NL {yy.getLogger().debug('Stop NL2 ');}
|
||||
| stop EOF {yy.getLogger().debug('Stop EOF2 ');}
|
||||
;
|
||||
|
||||
//array of statements
|
||||
document
|
||||
: statement { yy.getLogger().debug("Rule: statement: ", $1); typeof $1.length === 'number'?$$ = $1:$$ = [$1]; }
|
||||
| statement document { yy.getLogger().debug("Rule: statement #2: ", $1); $$ = [$1].concat($2); }
|
||||
;
|
||||
|
||||
link
|
||||
: LINK
|
||||
{ yy.getLogger().debug("Rule: link: ", $1, yytext); $$={edgeTypeStr: $1, label:''}; }
|
||||
| START_LINK LINK_LABEL STR LINK
|
||||
{ yy.getLogger().debug("Rule: LABEL link: ", $1, $3, $4); $$={edgeTypeStr: $4, label:$3}; }
|
||||
;
|
||||
|
||||
statement
|
||||
: nodeStatement
|
||||
| columnsStatement
|
||||
| SPACE_BLOCK
|
||||
{ const num=parseInt($1); const spaceId = yy.generateId(); $$ = { id: spaceId, type:'space', label:'', width: num, children: [] }}
|
||||
| blockStatement
|
||||
| classDefStatement
|
||||
| cssClassStatement
|
||||
| styleStatement
|
||||
;
|
||||
|
||||
nodeStatement
|
||||
: nodeStatement link node {
|
||||
yy.getLogger().debug('Rule: (nodeStatement link node) ', $1, $2, $3, ' typestr: ',$2.edgeTypeStr);
|
||||
const edgeData = yy.edgeStrToEdgeData($2.edgeTypeStr)
|
||||
$$ = [
|
||||
{id: $1.id, label: $1.label, type:$1.type, directions: $1.directions},
|
||||
{id: $1.id + '-' + $3.id, start: $1.id, end: $3.id, label: $2.label, type: 'edge', directions: $3.directions, arrowTypeEnd: edgeData, arrowTypeStart: 'arrow_open' },
|
||||
{id: $3.id, label: $3.label, type: yy.typeStr2Type($3.typeStr), directions: $3.directions}
|
||||
];
|
||||
}
|
||||
| node SIZE { yy.getLogger().debug('Rule: nodeStatement (abc88 node size) ', $1, $2); $$ = {id: $1.id, label: $1.label, type: yy.typeStr2Type($1.typeStr), directions: $1.directions, widthInColumns: parseInt($2,10)}; }
|
||||
| node { yy.getLogger().debug('Rule: nodeStatement (node) ', $1); $$ = {id: $1.id, label: $1.label, type: yy.typeStr2Type($1.typeStr), directions: $1.directions, widthInColumns:1}; }
|
||||
;
|
||||
|
||||
|
||||
columnsStatement
|
||||
: COLUMNS { yy.getLogger().debug('APA123', this? this:'na'); yy.getLogger().debug("COLUMNS: ", $1); $$ = {type: 'column-setting', columns: $1 === 'auto'?-1:parseInt($1) } }
|
||||
;
|
||||
|
||||
blockStatement
|
||||
: id-block nodeStatement document end { yy.getLogger().debug('Rule: id-block statement : ', $2, $3); const id2 = yy.generateId(); $$ = { ...$2, type:'composite', children: $3 }; }
|
||||
| block document end { yy.getLogger().debug('Rule: blockStatement : ', $1, $2, $3); const id = yy.generateId(); $$ = { id, type:'composite', label:'', children: $2 }; }
|
||||
;
|
||||
|
||||
node
|
||||
: NODE_ID
|
||||
{ yy.getLogger().debug("Rule: node (NODE_ID separator): ", $1); $$ = { id: $1 }; }
|
||||
| NODE_ID nodeShapeNLabel
|
||||
{
|
||||
yy.getLogger().debug("Rule: node (NODE_ID nodeShapeNLabel separator): ", $1, $2);
|
||||
$$ = { id: $1, label: $2.label, typeStr: $2.typeStr, directions: $2.directions };
|
||||
}
|
||||
;
|
||||
|
||||
dirList: DIR { yy.getLogger().debug("Rule: dirList: ", $1); $$ = [$1]; }
|
||||
| DIR dirList { yy.getLogger().debug("Rule: dirList: ", $1, $2); $$ = [$1].concat($2); }
|
||||
;
|
||||
|
||||
nodeShapeNLabel
|
||||
: NODE_DSTART STR NODE_DEND
|
||||
{ yy.getLogger().debug("Rule: nodeShapeNLabel: ", $1, $2, $3); $$ = { typeStr: $1 + $3, label: $2 }; }
|
||||
| BLOCK_ARROW_START STR dirList BLOCK_ARROW_END
|
||||
{ yy.getLogger().debug("Rule: BLOCK_ARROW nodeShapeNLabel: ", $1, $2, " #3:",$3, $4); $$ = { typeStr: $1 + $4, label: $2, directions: $3}; }
|
||||
;
|
||||
|
||||
|
||||
classDefStatement
|
||||
: classDef CLASSDEF_ID CLASSDEF_STYLEOPTS {
|
||||
$$ = { type: 'classDef', id: $2.trim(), css: $3.trim() };
|
||||
}
|
||||
| classDef DEFAULT CLASSDEF_STYLEOPTS {
|
||||
$$ = { type: 'classDef', id: $2.trim(), css: $3.trim() };
|
||||
}
|
||||
;
|
||||
|
||||
cssClassStatement
|
||||
: class CLASSENTITY_IDS STYLECLASS {
|
||||
//log.debug('apply class: id(s): ',$2, ' style class: ', $3);
|
||||
$$={ type: 'applyClass', id: $2.trim(), styleClass: $3.trim() };
|
||||
}
|
||||
;
|
||||
|
||||
styleStatement
|
||||
: style STYLE_ENTITY_IDS STYLE_DEFINITION_DATA {
|
||||
$$={ type: 'applyStyles', id: $2.trim(), stylesStr: $3.trim() };
|
||||
}
|
||||
;
|
||||
|
||||
%%
|
||||
409
packages/mermaid/src/diagrams/block/parser/block.spec.ts
Normal file
409
packages/mermaid/src/diagrams/block/parser/block.spec.ts
Normal file
@@ -0,0 +1,409 @@
|
||||
// @ts-ignore: jison doesn't export types
|
||||
import block from './block.jison';
|
||||
import db from '../blockDB.js';
|
||||
import { cleanupComments } from '../../../diagram-api/comments.js';
|
||||
import { prepareTextForParsing } from '../blockUtils.js';
|
||||
import { setConfig } from '../../../config.js';
|
||||
|
||||
describe('Block diagram', function () {
|
||||
describe('when parsing an block diagram graph it should handle > ', function () {
|
||||
beforeEach(function () {
|
||||
block.parser.yy = db;
|
||||
block.parser.yy.clear();
|
||||
block.parser.yy.getLogger = () => console;
|
||||
});
|
||||
|
||||
it('a diagram with a node', async () => {
|
||||
const str = `block-beta
|
||||
id
|
||||
`;
|
||||
|
||||
block.parse(str);
|
||||
const blocks = db.getBlocks();
|
||||
expect(blocks.length).toBe(1);
|
||||
expect(blocks[0].id).toBe('id');
|
||||
expect(blocks[0].label).toBe('id');
|
||||
});
|
||||
it('a node with a square shape and a label', async () => {
|
||||
const str = `block-beta
|
||||
id["A label"]
|
||||
`;
|
||||
|
||||
block.parse(str);
|
||||
const blocks = db.getBlocks();
|
||||
expect(blocks.length).toBe(1);
|
||||
expect(blocks[0].id).toBe('id');
|
||||
expect(blocks[0].label).toBe('A label');
|
||||
expect(blocks[0].type).toBe('square');
|
||||
});
|
||||
it('a diagram with multiple nodes', async () => {
|
||||
const str = `block-beta
|
||||
id1
|
||||
id2
|
||||
`;
|
||||
|
||||
block.parse(str);
|
||||
const blocks = db.getBlocks();
|
||||
expect(blocks.length).toBe(2);
|
||||
expect(blocks[0].id).toBe('id1');
|
||||
expect(blocks[0].label).toBe('id1');
|
||||
expect(blocks[0].type).toBe('na');
|
||||
expect(blocks[1].id).toBe('id2');
|
||||
expect(blocks[1].label).toBe('id2');
|
||||
expect(blocks[1].type).toBe('na');
|
||||
});
|
||||
it('a diagram with multiple nodes', async () => {
|
||||
const str = `block-beta
|
||||
id1
|
||||
id2
|
||||
id3
|
||||
`;
|
||||
|
||||
block.parse(str);
|
||||
const blocks = db.getBlocks();
|
||||
expect(blocks.length).toBe(3);
|
||||
expect(blocks[0].id).toBe('id1');
|
||||
expect(blocks[0].label).toBe('id1');
|
||||
expect(blocks[0].type).toBe('na');
|
||||
expect(blocks[1].id).toBe('id2');
|
||||
expect(blocks[1].label).toBe('id2');
|
||||
expect(blocks[1].type).toBe('na');
|
||||
expect(blocks[2].id).toBe('id3');
|
||||
expect(blocks[2].label).toBe('id3');
|
||||
expect(blocks[2].type).toBe('na');
|
||||
});
|
||||
|
||||
it('a node with a square shape and a label', async () => {
|
||||
const str = `block-beta
|
||||
id["A label"]
|
||||
id2`;
|
||||
|
||||
block.parse(str);
|
||||
const blocks = db.getBlocks();
|
||||
expect(blocks.length).toBe(2);
|
||||
expect(blocks[0].id).toBe('id');
|
||||
expect(blocks[0].label).toBe('A label');
|
||||
expect(blocks[0].type).toBe('square');
|
||||
expect(blocks[1].id).toBe('id2');
|
||||
expect(blocks[1].label).toBe('id2');
|
||||
expect(blocks[1].type).toBe('na');
|
||||
});
|
||||
it('a diagram with multiple nodes with edges abc123', async () => {
|
||||
const str = `block-beta
|
||||
id1["first"] --> id2["second"]
|
||||
`;
|
||||
|
||||
block.parse(str);
|
||||
const blocks = db.getBlocks();
|
||||
const edges = db.getEdges();
|
||||
expect(blocks.length).toBe(2);
|
||||
expect(edges.length).toBe(1);
|
||||
expect(edges[0].start).toBe('id1');
|
||||
expect(edges[0].end).toBe('id2');
|
||||
expect(edges[0].arrowTypeEnd).toBe('arrow_point');
|
||||
});
|
||||
it('a diagram with multiple nodes with edges abc123', async () => {
|
||||
const str = `block-beta
|
||||
id1["first"] -- "a label" --> id2["second"]
|
||||
`;
|
||||
|
||||
block.parse(str);
|
||||
const blocks = db.getBlocks();
|
||||
const edges = db.getEdges();
|
||||
expect(blocks.length).toBe(2);
|
||||
expect(edges.length).toBe(1);
|
||||
expect(edges[0].start).toBe('id1');
|
||||
expect(edges[0].end).toBe('id2');
|
||||
expect(edges[0].arrowTypeEnd).toBe('arrow_point');
|
||||
expect(edges[0].label).toBe('a label');
|
||||
});
|
||||
it('a diagram with column statements', async () => {
|
||||
const str = `block-beta
|
||||
columns 2
|
||||
block1["Block 1"]
|
||||
`;
|
||||
|
||||
block.parse(str);
|
||||
expect(db.getColumns('root')).toBe(2);
|
||||
const blocks = db.getBlocks();
|
||||
expect(blocks.length).toBe(1);
|
||||
});
|
||||
it('a diagram withput column statements', async () => {
|
||||
const str = `block-beta
|
||||
block1["Block 1"]
|
||||
`;
|
||||
|
||||
block.parse(str);
|
||||
expect(db.getColumns('root')).toBe(-1);
|
||||
const blocks = db.getBlocks();
|
||||
expect(blocks.length).toBe(1);
|
||||
});
|
||||
it('a diagram with auto column statements', async () => {
|
||||
const str = `block-beta
|
||||
columns auto
|
||||
block1["Block 1"]
|
||||
`;
|
||||
|
||||
block.parse(str);
|
||||
expect(db.getColumns('root')).toBe(-1);
|
||||
const blocks = db.getBlocks();
|
||||
expect(blocks.length).toBe(1);
|
||||
});
|
||||
|
||||
it('blocks next to each other', async () => {
|
||||
const str = `block-beta
|
||||
columns 2
|
||||
block1["Block 1"]
|
||||
block2["Block 2"]
|
||||
`;
|
||||
|
||||
block.parse(str);
|
||||
|
||||
const blocks = db.getBlocks();
|
||||
expect(db.getColumns('root')).toBe(2);
|
||||
expect(blocks.length).toBe(2);
|
||||
});
|
||||
|
||||
it('blocks on top of each other', async () => {
|
||||
const str = `block-beta
|
||||
columns 1
|
||||
block1["Block 1"]
|
||||
block2["Block 2"]
|
||||
`;
|
||||
|
||||
block.parse(str);
|
||||
|
||||
const blocks = db.getBlocks();
|
||||
expect(db.getColumns('root')).toBe(1);
|
||||
expect(blocks.length).toBe(2);
|
||||
});
|
||||
|
||||
it('compound blocks 2', async () => {
|
||||
const str = `block-beta
|
||||
block
|
||||
aBlock["ABlock"]
|
||||
bBlock["BBlock"]
|
||||
end
|
||||
`;
|
||||
|
||||
block.parse(str);
|
||||
const blocks = db.getBlocks();
|
||||
expect(blocks.length).toBe(1);
|
||||
|
||||
expect(blocks[0].children.length).toBe(2);
|
||||
expect(blocks[0].id).not.toBe(undefined);
|
||||
expect(blocks[0].label).toBe('');
|
||||
expect(blocks[0].type).toBe('composite');
|
||||
|
||||
const aBlock = blocks[0].children[0];
|
||||
|
||||
expect(aBlock.id).not.toBe(aBlock);
|
||||
expect(aBlock.label).toBe('ABlock');
|
||||
expect(aBlock.type).toBe('square');
|
||||
|
||||
const bBlock = blocks[0].children[1];
|
||||
expect(bBlock.id).not.toBe(bBlock);
|
||||
expect(bBlock.label).toBe('BBlock');
|
||||
expect(bBlock.type).toBe('square');
|
||||
});
|
||||
it('compound blocks of compound blocks', async () => {
|
||||
const str = `block-beta
|
||||
block
|
||||
aBlock["ABlock"]
|
||||
block
|
||||
bBlock["BBlock"]
|
||||
end
|
||||
end
|
||||
`;
|
||||
|
||||
block.parse(str);
|
||||
const blocks = db.getBlocks();
|
||||
|
||||
const aBlock = blocks[0].children[0];
|
||||
const secondComposite = blocks[0].children[1];
|
||||
const bBlock = blocks[0].children[1].children[0];
|
||||
|
||||
expect(blocks[0].children.length).toBe(2);
|
||||
expect(blocks[0].id).not.toBe(undefined);
|
||||
expect(blocks[0].label).toBe('');
|
||||
expect(blocks[0].type).toBe('composite');
|
||||
|
||||
expect(secondComposite.children.length).toBe(1);
|
||||
expect(secondComposite.id).not.toBe(undefined);
|
||||
expect(secondComposite.label).toBe('');
|
||||
expect(secondComposite.type).toBe('composite');
|
||||
|
||||
expect(aBlock.id).not.toBe(aBlock);
|
||||
expect(aBlock.label).toBe('ABlock');
|
||||
expect(aBlock.type).toBe('square');
|
||||
|
||||
expect(bBlock.id).not.toBe(bBlock);
|
||||
expect(bBlock.label).toBe('BBlock');
|
||||
expect(bBlock.type).toBe('square');
|
||||
});
|
||||
it('compound blocks with title', async () => {
|
||||
const str = `block-beta
|
||||
block:compoundBlock["Compound block"]
|
||||
columns 1
|
||||
block2["Block 2"]
|
||||
end
|
||||
`;
|
||||
|
||||
block.parse(str);
|
||||
|
||||
const blocks = db.getBlocks();
|
||||
expect(blocks.length).toBe(1);
|
||||
|
||||
const compoundBlock = blocks[0];
|
||||
const block2 = compoundBlock.children[0];
|
||||
|
||||
expect(compoundBlock.children.length).toBe(1);
|
||||
expect(compoundBlock.id).toBe('compoundBlock');
|
||||
expect(compoundBlock.label).toBe('Compound block');
|
||||
expect(compoundBlock.type).toBe('composite');
|
||||
|
||||
expect(block2.id).toBe('block2');
|
||||
expect(block2.label).toBe('Block 2');
|
||||
expect(block2.type).toBe('square');
|
||||
});
|
||||
it('blocks mixed with compound blocks', async () => {
|
||||
const str = `block-beta
|
||||
columns 1
|
||||
block1["Block 1"]
|
||||
|
||||
block
|
||||
columns 2
|
||||
block2["Block 2"]
|
||||
block3["Block 3"]
|
||||
end
|
||||
`;
|
||||
|
||||
block.parse(str);
|
||||
|
||||
const blocks = db.getBlocks();
|
||||
expect(blocks.length).toBe(2);
|
||||
|
||||
const compoundBlock = blocks[1];
|
||||
const block2 = compoundBlock.children[0];
|
||||
|
||||
expect(compoundBlock.children.length).toBe(2);
|
||||
|
||||
expect(block2.id).toBe('block2');
|
||||
expect(block2.label).toBe('Block 2');
|
||||
expect(block2.type).toBe('square');
|
||||
});
|
||||
|
||||
it('Arrow blocks', async () => {
|
||||
const str = `block-beta
|
||||
columns 3
|
||||
block1["Block 1"]
|
||||
blockArrow<[" "]>(right)
|
||||
block2["Block 2"]`;
|
||||
|
||||
block.parse(str);
|
||||
|
||||
const blocks = db.getBlocks();
|
||||
expect(blocks.length).toBe(3);
|
||||
|
||||
const block1 = blocks[0];
|
||||
const blockArrow = blocks[1];
|
||||
const block2 = blocks[2];
|
||||
|
||||
expect(block1.id).toBe('block1');
|
||||
expect(blockArrow.id).toBe('blockArrow');
|
||||
expect(block2.id).toBe('block2');
|
||||
expect(block2.label).toBe('Block 2');
|
||||
expect(block2.type).toBe('square');
|
||||
expect(blockArrow.type).toBe('block_arrow');
|
||||
expect(blockArrow.directions).toContain('right');
|
||||
});
|
||||
it('Arrow blocks with multiple points', async () => {
|
||||
const str = `block-beta
|
||||
columns 1
|
||||
A
|
||||
blockArrow<[" "]>(up, down)
|
||||
block
|
||||
columns 3
|
||||
B
|
||||
C
|
||||
D
|
||||
end`;
|
||||
|
||||
block.parse(str);
|
||||
|
||||
const blocks = db.getBlocks();
|
||||
expect(blocks.length).toBe(3);
|
||||
|
||||
const blockArrow = blocks[1];
|
||||
expect(blockArrow.type).toBe('block_arrow');
|
||||
expect(blockArrow.directions).toContain('up');
|
||||
expect(blockArrow.directions).toContain('down');
|
||||
expect(blockArrow.directions).not.toContain('right');
|
||||
});
|
||||
it('blocks with different widths', async () => {
|
||||
const str = `block-beta
|
||||
columns 3
|
||||
one["One Slot"]
|
||||
two["Two slots"]:2
|
||||
`;
|
||||
|
||||
block.parse(str);
|
||||
|
||||
const blocks = db.getBlocks();
|
||||
expect(blocks.length).toBe(2);
|
||||
const one = blocks[0];
|
||||
const two = blocks[1];
|
||||
expect(two.widthInColumns).toBe(2);
|
||||
});
|
||||
it('empty blocks', async () => {
|
||||
const str = `block-beta
|
||||
columns 3
|
||||
space
|
||||
middle["In the middle"]
|
||||
space
|
||||
`;
|
||||
|
||||
block.parse(str);
|
||||
|
||||
const blocks = db.getBlocks();
|
||||
expect(blocks.length).toBe(3);
|
||||
const sp1 = blocks[0];
|
||||
const middle = blocks[1];
|
||||
const sp2 = blocks[2];
|
||||
expect(sp1.type).toBe('space');
|
||||
expect(sp2.type).toBe('space');
|
||||
expect(middle.label).toBe('In the middle');
|
||||
});
|
||||
it('classDef statements applied to a block', async () => {
|
||||
const str = `block-beta
|
||||
classDef black color:#ffffff, fill:#000000;
|
||||
|
||||
mc["Memcache"]
|
||||
class mc black
|
||||
`;
|
||||
|
||||
block.parse(str);
|
||||
const blocks = db.getBlocks();
|
||||
expect(blocks.length).toBe(1);
|
||||
const mc = blocks[0];
|
||||
expect(mc.classes).toContain('black');
|
||||
const classes = db.getClasses();
|
||||
const black = classes.black;
|
||||
expect(black.id).toBe('black');
|
||||
expect(black.styles[0]).toEqual('color:#ffffff');
|
||||
});
|
||||
it('style statements applied to a block', async () => {
|
||||
const str = `block-beta
|
||||
columns 1
|
||||
B["A wide one in the middle"]
|
||||
style B fill:#f9F,stroke:#333,stroke-width:4px
|
||||
`;
|
||||
|
||||
block.parse(str);
|
||||
const blocks = db.getBlocks();
|
||||
expect(blocks.length).toBe(1);
|
||||
const B = blocks[0];
|
||||
expect(B.styles).toContain('fill:#f9F');
|
||||
});
|
||||
});
|
||||
});
|
||||
256
packages/mermaid/src/diagrams/block/renderHelpers.ts
Normal file
256
packages/mermaid/src/diagrams/block/renderHelpers.ts
Normal file
@@ -0,0 +1,256 @@
|
||||
import * as graphlib from 'dagre-d3-es/src/graphlib/index.js';
|
||||
import { getConfig } from '../../config.js';
|
||||
import { insertEdge, insertEdgeLabel, positionEdgeLabel } from '../../dagre-wrapper/edges.js';
|
||||
import { insertNode, positionNode } from '../../dagre-wrapper/nodes.js';
|
||||
import { getStylesFromArray } from '../../utils.js';
|
||||
import type { BlockDB } from './blockDB.js';
|
||||
import type { Block } from './blockTypes.js';
|
||||
|
||||
function getNodeFromBlock(block: Block, db: BlockDB, positioned = false) {
|
||||
const vertex = block;
|
||||
|
||||
let classStr = 'default';
|
||||
if ((vertex?.classes?.length || 0) > 0) {
|
||||
classStr = (vertex?.classes || []).join(' ');
|
||||
}
|
||||
classStr = classStr + ' flowchart-label';
|
||||
|
||||
// We create a SVG label, either by delegating to addHtmlLabel or manually
|
||||
let radius = 0;
|
||||
let shape = '';
|
||||
let layoutOptions = {};
|
||||
let padding;
|
||||
// Set the shape based parameters
|
||||
switch (vertex.type) {
|
||||
case 'round':
|
||||
radius = 5;
|
||||
shape = 'rect';
|
||||
break;
|
||||
case 'composite':
|
||||
radius = 0;
|
||||
shape = 'composite';
|
||||
padding = 0;
|
||||
break;
|
||||
case 'square':
|
||||
shape = 'rect';
|
||||
break;
|
||||
case 'diamond':
|
||||
shape = 'question';
|
||||
layoutOptions = {
|
||||
portConstraints: 'FIXED_SIDE',
|
||||
};
|
||||
break;
|
||||
case 'hexagon':
|
||||
shape = 'hexagon';
|
||||
break;
|
||||
case 'block_arrow':
|
||||
shape = 'block_arrow';
|
||||
break;
|
||||
case 'odd':
|
||||
shape = 'rect_left_inv_arrow';
|
||||
break;
|
||||
case 'lean_right':
|
||||
shape = 'lean_right';
|
||||
break;
|
||||
case 'lean_left':
|
||||
shape = 'lean_left';
|
||||
break;
|
||||
case 'trapezoid':
|
||||
shape = 'trapezoid';
|
||||
break;
|
||||
case 'inv_trapezoid':
|
||||
shape = 'inv_trapezoid';
|
||||
break;
|
||||
case 'rect_left_inv_arrow':
|
||||
shape = 'rect_left_inv_arrow';
|
||||
break;
|
||||
case 'circle':
|
||||
shape = 'circle';
|
||||
break;
|
||||
case 'ellipse':
|
||||
shape = 'ellipse';
|
||||
break;
|
||||
case 'stadium':
|
||||
shape = 'stadium';
|
||||
break;
|
||||
case 'subroutine':
|
||||
shape = 'subroutine';
|
||||
break;
|
||||
case 'cylinder':
|
||||
shape = 'cylinder';
|
||||
break;
|
||||
case 'group':
|
||||
shape = 'rect';
|
||||
break;
|
||||
case 'doublecircle':
|
||||
shape = 'doublecircle';
|
||||
break;
|
||||
default:
|
||||
shape = 'rect';
|
||||
}
|
||||
|
||||
const styles = getStylesFromArray(vertex?.styles || []);
|
||||
|
||||
// Use vertex id as text in the box if no text is provided by the graph definition
|
||||
const vertexText = vertex.label;
|
||||
|
||||
const bounds = vertex.size || { width: 0, height: 0, x: 0, y: 0 };
|
||||
// Add the node
|
||||
const node = {
|
||||
labelStyle: styles.labelStyle,
|
||||
shape: shape,
|
||||
labelText: vertexText,
|
||||
rx: radius,
|
||||
ry: radius,
|
||||
class: classStr,
|
||||
style: styles.style,
|
||||
id: vertex.id,
|
||||
directions: vertex.directions,
|
||||
width: bounds.width,
|
||||
height: bounds.height,
|
||||
x: bounds.x,
|
||||
y: bounds.y,
|
||||
positioned,
|
||||
intersect: undefined,
|
||||
type: vertex.type,
|
||||
padding: padding ?? (getConfig()?.block?.padding || 0),
|
||||
};
|
||||
return node;
|
||||
}
|
||||
async function calculateBlockSize(
|
||||
elem: d3.Selection<SVGGElement, unknown, HTMLElement, any>,
|
||||
block: any,
|
||||
db: any
|
||||
) {
|
||||
const node = getNodeFromBlock(block, db, false);
|
||||
if (node.type === 'group') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Add the element to the DOM to size it
|
||||
const nodeEl = await insertNode(elem, node);
|
||||
const boundingBox = nodeEl.node().getBBox();
|
||||
const obj = db.getBlock(node.id);
|
||||
obj.size = { width: boundingBox.width, height: boundingBox.height, x: 0, y: 0, node: nodeEl };
|
||||
db.setBlock(obj);
|
||||
nodeEl.remove();
|
||||
}
|
||||
type ActionFun = typeof calculateBlockSize;
|
||||
|
||||
export async function insertBlockPositioned(elem: any, block: Block, db: any) {
|
||||
const node = getNodeFromBlock(block, db, true);
|
||||
// Add the element to the DOM to size it
|
||||
const obj = db.getBlock(node.id);
|
||||
if (obj.type !== 'space') {
|
||||
const nodeEl = await insertNode(elem, node);
|
||||
block.intersect = node?.intersect;
|
||||
positionNode(node);
|
||||
}
|
||||
}
|
||||
|
||||
export async function performOperations(
|
||||
elem: d3.Selection<SVGGElement, unknown, HTMLElement, any>,
|
||||
blocks: Block[],
|
||||
db: BlockDB,
|
||||
operation: ActionFun
|
||||
) {
|
||||
for (const block of blocks) {
|
||||
await operation(elem, block, db);
|
||||
if (block.children) {
|
||||
await performOperations(elem, block.children, db, operation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function calculateBlockSizes(elem: any, blocks: Block[], db: BlockDB) {
|
||||
await performOperations(elem, blocks, db, calculateBlockSize);
|
||||
}
|
||||
|
||||
export async function insertBlocks(
|
||||
elem: d3.Selection<SVGGElement, unknown, HTMLElement, any>,
|
||||
blocks: Block[],
|
||||
db: BlockDB
|
||||
) {
|
||||
await performOperations(elem, blocks, db, insertBlockPositioned);
|
||||
}
|
||||
|
||||
export async function insertEdges(
|
||||
elem: any,
|
||||
edges: Block[],
|
||||
blocks: Block[],
|
||||
db: BlockDB,
|
||||
id: string
|
||||
) {
|
||||
const g = new graphlib.Graph({
|
||||
multigraph: true,
|
||||
compound: true,
|
||||
});
|
||||
g.setGraph({
|
||||
rankdir: 'TB',
|
||||
nodesep: 10,
|
||||
ranksep: 10,
|
||||
marginx: 8,
|
||||
marginy: 8,
|
||||
});
|
||||
|
||||
for (const block of blocks) {
|
||||
if (block.size) {
|
||||
g.setNode(block.id, {
|
||||
width: block.size.width,
|
||||
height: block.size.height,
|
||||
intersect: block.intersect,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const edge of edges) {
|
||||
// elem, e, edge, clusterDb, diagramType, graph;
|
||||
if (edge.start && edge.end) {
|
||||
const startBlock = db.getBlock(edge.start);
|
||||
const endBlock = db.getBlock(edge.end);
|
||||
|
||||
if (startBlock?.size && endBlock?.size) {
|
||||
const start = startBlock.size;
|
||||
const end = endBlock.size;
|
||||
const points = [
|
||||
{ x: start.x, y: start.y },
|
||||
{ x: start.x + (end.x - start.x) / 2, y: start.y + (end.y - start.y) / 2 },
|
||||
{ x: end.x, y: end.y },
|
||||
];
|
||||
// edge.points = points;
|
||||
await insertEdge(
|
||||
elem,
|
||||
{ v: edge.start, w: edge.end, name: edge.id },
|
||||
{
|
||||
...edge,
|
||||
arrowTypeEnd: edge.arrowTypeEnd,
|
||||
arrowTypeStart: edge.arrowTypeStart,
|
||||
points,
|
||||
classes: 'edge-thickness-normal edge-pattern-solid flowchart-link LS-a1 LE-b1',
|
||||
},
|
||||
undefined,
|
||||
'block',
|
||||
g,
|
||||
id
|
||||
);
|
||||
if (edge.label) {
|
||||
await insertEdgeLabel(elem, {
|
||||
...edge,
|
||||
label: edge.label,
|
||||
labelStyle: 'stroke: #333; stroke-width: 1.5px;fill:none;',
|
||||
arrowTypeEnd: edge.arrowTypeEnd,
|
||||
arrowTypeStart: edge.arrowTypeStart,
|
||||
points,
|
||||
classes: 'edge-thickness-normal edge-pattern-solid flowchart-link LS-a1 LE-b1',
|
||||
});
|
||||
await positionEdgeLabel(
|
||||
{ ...edge, x: points[1].x, y: points[1].y },
|
||||
{
|
||||
originalPath: points,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
147
packages/mermaid/src/diagrams/block/styles.ts
Normal file
147
packages/mermaid/src/diagrams/block/styles.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
import * as khroma from 'khroma';
|
||||
|
||||
/** Returns the styles given options */
|
||||
export interface BlockChartStyleOptions {
|
||||
arrowheadColor: string;
|
||||
border2: string;
|
||||
clusterBkg: string;
|
||||
clusterBorder: string;
|
||||
edgeLabelBackground: string;
|
||||
fontFamily: string;
|
||||
lineColor: string;
|
||||
mainBkg: string;
|
||||
nodeBorder: string;
|
||||
nodeTextColor: string;
|
||||
tertiaryColor: string;
|
||||
textColor: string;
|
||||
titleColor: string;
|
||||
}
|
||||
|
||||
const fade = (color: string, opacity: number) => {
|
||||
// @ts-ignore TODO: incorrect types from khroma
|
||||
const channel = khroma.channel;
|
||||
|
||||
const r = channel(color, 'r');
|
||||
const g = channel(color, 'g');
|
||||
const b = channel(color, 'b');
|
||||
|
||||
// @ts-ignore incorrect types from khroma
|
||||
return khroma.rgba(r, g, b, opacity);
|
||||
};
|
||||
|
||||
const getStyles = (options: BlockChartStyleOptions) =>
|
||||
`.label {
|
||||
font-family: ${options.fontFamily};
|
||||
color: ${options.nodeTextColor || options.textColor};
|
||||
}
|
||||
.cluster-label text {
|
||||
fill: ${options.titleColor};
|
||||
}
|
||||
.cluster-label span,p {
|
||||
color: ${options.titleColor};
|
||||
}
|
||||
|
||||
|
||||
|
||||
.label text,span,p {
|
||||
fill: ${options.nodeTextColor || options.textColor};
|
||||
color: ${options.nodeTextColor || options.textColor};
|
||||
}
|
||||
|
||||
.node rect,
|
||||
.node circle,
|
||||
.node ellipse,
|
||||
.node polygon,
|
||||
.node path {
|
||||
fill: ${options.mainBkg};
|
||||
stroke: ${options.nodeBorder};
|
||||
stroke-width: 1px;
|
||||
}
|
||||
.flowchart-label text {
|
||||
text-anchor: middle;
|
||||
}
|
||||
// .flowchart-label .text-outer-tspan {
|
||||
// text-anchor: middle;
|
||||
// }
|
||||
// .flowchart-label .text-inner-tspan {
|
||||
// text-anchor: start;
|
||||
// }
|
||||
|
||||
.node .label {
|
||||
text-align: center;
|
||||
}
|
||||
.node.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.arrowheadPath {
|
||||
fill: ${options.arrowheadColor};
|
||||
}
|
||||
|
||||
.edgePath .path {
|
||||
stroke: ${options.lineColor};
|
||||
stroke-width: 2.0px;
|
||||
}
|
||||
|
||||
.flowchart-link {
|
||||
stroke: ${options.lineColor};
|
||||
fill: none;
|
||||
}
|
||||
|
||||
.edgeLabel {
|
||||
background-color: ${options.edgeLabelBackground};
|
||||
rect {
|
||||
opacity: 0.5;
|
||||
background-color: ${options.edgeLabelBackground};
|
||||
fill: ${options.edgeLabelBackground};
|
||||
}
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* For html labels only */
|
||||
.labelBkg {
|
||||
background-color: ${fade(options.edgeLabelBackground, 0.5)};
|
||||
// background-color:
|
||||
}
|
||||
|
||||
.node .cluster {
|
||||
// fill: ${fade(options.mainBkg, 0.5)};
|
||||
fill: ${fade(options.clusterBkg, 0.5)};
|
||||
stroke: ${fade(options.clusterBorder, 0.2)};
|
||||
box-shadow: rgba(50, 50, 93, 0.25) 0px 13px 27px -5px, rgba(0, 0, 0, 0.3) 0px 8px 16px -8px;
|
||||
stroke-width: 1px;
|
||||
}
|
||||
|
||||
.cluster text {
|
||||
fill: ${options.titleColor};
|
||||
}
|
||||
|
||||
.cluster span,p {
|
||||
color: ${options.titleColor};
|
||||
}
|
||||
/* .cluster div {
|
||||
color: ${options.titleColor};
|
||||
} */
|
||||
|
||||
div.mermaidTooltip {
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
max-width: 200px;
|
||||
padding: 2px;
|
||||
font-family: ${options.fontFamily};
|
||||
font-size: 12px;
|
||||
background: ${options.tertiaryColor};
|
||||
border: 1px solid ${options.border2};
|
||||
border-radius: 2px;
|
||||
pointer-events: none;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.flowchartTitleText {
|
||||
text-anchor: middle;
|
||||
font-size: 18px;
|
||||
fill: ${options.textColor};
|
||||
}
|
||||
`;
|
||||
|
||||
export default getStyles;
|
||||
@@ -11,7 +11,7 @@ let c4ShapeArray = [];
|
||||
let boundaryParseStack = [''];
|
||||
let currentBoundaryParse = 'global';
|
||||
let parentBoundaryParse = '';
|
||||
let boundarys = [
|
||||
let boundaries = [
|
||||
{
|
||||
alias: 'global',
|
||||
label: { text: 'global' },
|
||||
@@ -312,12 +312,12 @@ export const addPersonOrSystemBoundary = function (alias, label, type, tags, lin
|
||||
}
|
||||
|
||||
let boundary = {};
|
||||
const old = boundarys.find((boundary) => boundary.alias === alias);
|
||||
const old = boundaries.find((boundary) => boundary.alias === alias);
|
||||
if (old && alias === old.alias) {
|
||||
boundary = old;
|
||||
} else {
|
||||
boundary.alias = alias;
|
||||
boundarys.push(boundary);
|
||||
boundaries.push(boundary);
|
||||
}
|
||||
|
||||
// Don't allow null labels, either
|
||||
@@ -368,12 +368,12 @@ export const addContainerBoundary = function (alias, label, type, tags, link) {
|
||||
}
|
||||
|
||||
let boundary = {};
|
||||
const old = boundarys.find((boundary) => boundary.alias === alias);
|
||||
const old = boundaries.find((boundary) => boundary.alias === alias);
|
||||
if (old && alias === old.alias) {
|
||||
boundary = old;
|
||||
} else {
|
||||
boundary.alias = alias;
|
||||
boundarys.push(boundary);
|
||||
boundaries.push(boundary);
|
||||
}
|
||||
|
||||
// Don't allow null labels, either
|
||||
@@ -433,12 +433,12 @@ export const addDeploymentNode = function (
|
||||
}
|
||||
|
||||
let boundary = {};
|
||||
const old = boundarys.find((boundary) => boundary.alias === alias);
|
||||
const old = boundaries.find((boundary) => boundary.alias === alias);
|
||||
if (old && alias === old.alias) {
|
||||
boundary = old;
|
||||
} else {
|
||||
boundary.alias = alias;
|
||||
boundarys.push(boundary);
|
||||
boundaries.push(boundary);
|
||||
}
|
||||
|
||||
// Don't allow null labels, either
|
||||
@@ -514,7 +514,7 @@ export const updateElStyle = function (
|
||||
) {
|
||||
let old = c4ShapeArray.find((element) => element.alias === elementName);
|
||||
if (old === undefined) {
|
||||
old = boundarys.find((element) => element.alias === elementName);
|
||||
old = boundaries.find((element) => element.alias === elementName);
|
||||
if (old === undefined) {
|
||||
return;
|
||||
}
|
||||
@@ -697,14 +697,19 @@ export const getC4ShapeKeys = function (parentBoundary) {
|
||||
return Object.keys(getC4ShapeArray(parentBoundary));
|
||||
};
|
||||
|
||||
export const getBoundarys = function (parentBoundary) {
|
||||
export const getBoundaries = function (parentBoundary) {
|
||||
if (parentBoundary === undefined || parentBoundary === null) {
|
||||
return boundarys;
|
||||
return boundaries;
|
||||
} else {
|
||||
return boundarys.filter((boundary) => boundary.parentBoundary === parentBoundary);
|
||||
return boundaries.filter((boundary) => boundary.parentBoundary === parentBoundary);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link getBoundaries} instead
|
||||
*/
|
||||
export const getBoundarys = getBoundaries;
|
||||
|
||||
export const getRels = function () {
|
||||
return rels;
|
||||
};
|
||||
@@ -723,7 +728,7 @@ export const autoWrap = function () {
|
||||
|
||||
export const clear = function () {
|
||||
c4ShapeArray = [];
|
||||
boundarys = [
|
||||
boundaries = [
|
||||
{
|
||||
alias: 'global',
|
||||
label: { text: 'global' },
|
||||
@@ -804,6 +809,7 @@ export default {
|
||||
getC4ShapeArray,
|
||||
getC4Shape,
|
||||
getC4ShapeKeys,
|
||||
getBoundaries,
|
||||
getBoundarys,
|
||||
getCurrentBoundaryParse,
|
||||
getParentBoundaryParse,
|
||||
|
||||
@@ -542,15 +542,15 @@ function drawInsideBoundary(
|
||||
);
|
||||
}
|
||||
parentBoundaryAlias = currentBoundary.alias;
|
||||
let nextCurrentBoundarys = diagObj.db.getBoundarys(parentBoundaryAlias);
|
||||
let nextCurrentBoundaries = diagObj.db.getBoundarys(parentBoundaryAlias);
|
||||
|
||||
if (nextCurrentBoundarys.length > 0) {
|
||||
if (nextCurrentBoundaries.length > 0) {
|
||||
// draw boundary inside currentBoundary
|
||||
drawInsideBoundary(
|
||||
diagram,
|
||||
parentBoundaryAlias,
|
||||
currentBounds,
|
||||
nextCurrentBoundarys,
|
||||
nextCurrentBoundaries,
|
||||
diagObj
|
||||
);
|
||||
}
|
||||
|
||||
@@ -687,3 +687,5 @@ export default {
|
||||
insertComputerIcon,
|
||||
insertClockIcon,
|
||||
};
|
||||
|
||||
// cspell:ignoreRegExp /'Mstartx.*/g
|
||||
|
||||
@@ -193,6 +193,7 @@ export const draw = function (text, id, _version, diagObj) {
|
||||
const relations = diagObj.db.getRelations();
|
||||
relations.forEach(function (relation) {
|
||||
log.info(
|
||||
// cspell:ignore tjoho
|
||||
'tjoho' + getGraphId(relation.id1) + getGraphId(relation.id2) + JSON.stringify(relation)
|
||||
);
|
||||
g.setEdge(
|
||||
|
||||
@@ -18,13 +18,18 @@ export const getRows = (s?: string): string[] => {
|
||||
return str.split('#br#');
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes script tags from a text
|
||||
*
|
||||
* @param txt - The text to sanitize
|
||||
* @returns The safer text
|
||||
*/
|
||||
export const removeScript = (txt: string): string => {
|
||||
const setupDompurifyHooksIfNotSetup = (() => {
|
||||
let setup = false;
|
||||
|
||||
return () => {
|
||||
if (!setup) {
|
||||
setupDompurifyHooks();
|
||||
setup = true;
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
function setupDompurifyHooks() {
|
||||
const TEMPORARY_ATTRIBUTE = 'data-temp-href-target';
|
||||
|
||||
DOMPurify.addHook('beforeSanitizeAttributes', (node: Element) => {
|
||||
@@ -33,8 +38,6 @@ export const removeScript = (txt: string): string => {
|
||||
}
|
||||
});
|
||||
|
||||
const sanitizedText = DOMPurify.sanitize(txt);
|
||||
|
||||
DOMPurify.addHook('afterSanitizeAttributes', (node: Element) => {
|
||||
if (node.tagName === 'A' && node.hasAttribute(TEMPORARY_ATTRIBUTE)) {
|
||||
node.setAttribute('target', node.getAttribute(TEMPORARY_ATTRIBUTE) || '');
|
||||
@@ -44,6 +47,18 @@ export const removeScript = (txt: string): string => {
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes script tags from a text
|
||||
*
|
||||
* @param txt - The text to sanitize
|
||||
* @returns The safer text
|
||||
*/
|
||||
export const removeScript = (txt: string): string => {
|
||||
setupDompurifyHooksIfNotSetup();
|
||||
|
||||
const sanitizedText = DOMPurify.sanitize(txt);
|
||||
|
||||
return sanitizedText;
|
||||
};
|
||||
@@ -274,6 +289,83 @@ const processSet = (input: string): string => {
|
||||
return chars.join('');
|
||||
};
|
||||
|
||||
// TODO: find a better method for detecting support. This interface was added in the MathML 4 spec.
|
||||
// Firefox versions between [4,71] (0.47%) and Safari versions between [5,13.4] (0.17%) don't have this interface implemented but MathML is supported
|
||||
export const isMathMLSupported = () => window.MathMLElement !== undefined;
|
||||
|
||||
export const katexRegex = /\$\$(.*)\$\$/g;
|
||||
|
||||
/**
|
||||
* Whether or not a text has KaTeX delimiters
|
||||
*
|
||||
* @param text - The text to test
|
||||
* @returns Whether or not the text has KaTeX delimiters
|
||||
*/
|
||||
export const hasKatex = (text: string): boolean => (text.match(katexRegex)?.length ?? 0) > 0;
|
||||
|
||||
/**
|
||||
* Computes the minimum dimensions needed to display a div containing MathML
|
||||
*
|
||||
* @param text - The text to test
|
||||
* @param config - Configuration for Mermaid
|
||||
* @returns Object containing \{width, height\}
|
||||
*/
|
||||
export const calculateMathMLDimensions = async (text: string, config: MermaidConfig) => {
|
||||
text = await renderKatex(text, config);
|
||||
const divElem = document.createElement('div');
|
||||
divElem.innerHTML = text;
|
||||
divElem.id = 'katex-temp';
|
||||
divElem.style.visibility = 'hidden';
|
||||
divElem.style.position = 'absolute';
|
||||
divElem.style.top = '0';
|
||||
const body = document.querySelector('body');
|
||||
body?.insertAdjacentElement('beforeend', divElem);
|
||||
const dim = { width: divElem.clientWidth, height: divElem.clientHeight };
|
||||
divElem.remove();
|
||||
return dim;
|
||||
};
|
||||
|
||||
/**
|
||||
* Attempts to render and return the KaTeX portion of a string with MathML
|
||||
*
|
||||
* @param text - The text to test
|
||||
* @param config - Configuration for Mermaid
|
||||
* @returns String containing MathML if KaTeX is supported, or an error message if it is not and stylesheets aren't present
|
||||
*/
|
||||
export const renderKatex = async (text: string, config: MermaidConfig): Promise<string> => {
|
||||
if (!hasKatex(text)) {
|
||||
return text;
|
||||
}
|
||||
|
||||
if (!isMathMLSupported() && !config.legacyMathML) {
|
||||
return text.replace(katexRegex, 'MathML is unsupported in this environment.');
|
||||
}
|
||||
|
||||
const { default: katex } = await import('katex');
|
||||
return text
|
||||
.split(lineBreakRegex)
|
||||
.map((line) =>
|
||||
hasKatex(line)
|
||||
? `
|
||||
<div style="display: flex; align-items: center; justify-content: center; white-space: nowrap;">
|
||||
${line}
|
||||
</div>
|
||||
`
|
||||
: `<div>${line}</div>`
|
||||
)
|
||||
.join('')
|
||||
.replace(katexRegex, (_, c) =>
|
||||
katex
|
||||
.renderToString(c, {
|
||||
throwOnError: true,
|
||||
displayMode: true,
|
||||
output: isMathMLSupported() ? 'mathml' : 'htmlAndMathml',
|
||||
})
|
||||
.replace(/\n/g, ' ')
|
||||
.replace(/<annotation.*<\/annotation>/g, '')
|
||||
);
|
||||
};
|
||||
|
||||
export default {
|
||||
getRows,
|
||||
sanitizeText,
|
||||
|
||||
@@ -11,6 +11,7 @@ export interface RectData {
|
||||
ry?: number;
|
||||
attrs?: Record<string, string | number>;
|
||||
anchor?: string;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export interface Bound {
|
||||
|
||||
14
packages/mermaid/src/diagrams/common/populateCommonDb.ts
Normal file
14
packages/mermaid/src/diagrams/common/populateCommonDb.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import type { DiagramAST } from '@mermaid-js/parser';
|
||||
import type { DiagramDB } from '../../diagram-api/types.js';
|
||||
|
||||
export function populateCommonDb(ast: DiagramAST, db: DiagramDB) {
|
||||
if (ast.accDescr) {
|
||||
db.setAccDescription?.(ast.accDescr);
|
||||
}
|
||||
if (ast.accTitle) {
|
||||
db.setAccTitle?.(ast.accTitle);
|
||||
}
|
||||
if (ast.title) {
|
||||
db.setDiagramTitle?.(ast.title);
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,9 @@ export const drawRect = (element: SVG | Group, rectData: RectData): D3RectElemen
|
||||
rectElement.attr('stroke', rectData.stroke);
|
||||
rectElement.attr('width', rectData.width);
|
||||
rectElement.attr('height', rectData.height);
|
||||
if (rectData.name) {
|
||||
rectElement.attr('name', rectData.name);
|
||||
}
|
||||
rectData.rx !== undefined && rectElement.attr('rx', rectData.rx);
|
||||
rectData.ry !== undefined && rectElement.attr('ry', rectData.ry);
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ const diagram: DiagramDefinition = {
|
||||
db: {},
|
||||
renderer,
|
||||
parser: {
|
||||
parser: { yy: {} },
|
||||
parse: (): void => {
|
||||
return;
|
||||
},
|
||||
|
||||
@@ -4,20 +4,20 @@ import { selectSvgElement } from '../../rendering-util/selectSvgElement.js';
|
||||
import { configureSvgSize } from '../../setupGraphViewbox.js';
|
||||
|
||||
/**
|
||||
* Draws a an info picture in the tag with id: id based on the graph definition in text.
|
||||
* Draws 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 version - The version
|
||||
*/
|
||||
export const draw = (_text: string, id: string, version: string) => {
|
||||
log.debug('renering svg for syntax error\n');
|
||||
|
||||
log.debug('rendering svg for syntax error\n');
|
||||
const svg: SVG = selectSvgElement(id);
|
||||
const g: Group = svg.append('g');
|
||||
|
||||
svg.attr('viewBox', '0 0 2412 512');
|
||||
configureSvgSize(svg, 100, 512, true);
|
||||
|
||||
const g: Group = svg.append('g');
|
||||
g.append('path')
|
||||
.attr('class', 'error-icon')
|
||||
.attr(
|
||||
|
||||
@@ -3,6 +3,7 @@ import type {
|
||||
DiagramDetector,
|
||||
DiagramLoader,
|
||||
} from '../../../diagram-api/types.js';
|
||||
import { log } from '../../../logger.js';
|
||||
|
||||
const id = 'flowchart-elk';
|
||||
|
||||
@@ -13,13 +14,21 @@ const detector: DiagramDetector = (txt, config): boolean => {
|
||||
// If a flowchart/graph diagram has their default renderer set to elk
|
||||
(/^\s*flowchart|graph/.test(txt) && config?.flowchart?.defaultRenderer === 'elk')
|
||||
) {
|
||||
// This will log at the end, hopefully.
|
||||
setTimeout(
|
||||
() =>
|
||||
log.warn(
|
||||
'flowchart-elk was moved to an external package in Mermaid v11. Please refer [release notes](link) for more details. This diagram will be rendered using `dagre` layout as a fallback.'
|
||||
),
|
||||
500
|
||||
);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const loader: DiagramLoader = async () => {
|
||||
const { diagram } = await import('./flowchart-elk-definition.js');
|
||||
const { diagram } = await import('../flowDiagram-v2.js');
|
||||
return { id, diagram };
|
||||
};
|
||||
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
// @ts-ignore: JISON typing missing
|
||||
import parser from '../parser/flow.jison';
|
||||
|
||||
import * as db from '../flowDb.js';
|
||||
import renderer from './flowRenderer-elk.js';
|
||||
import styles from './styles.js';
|
||||
|
||||
export const diagram = {
|
||||
db,
|
||||
renderer,
|
||||
parser,
|
||||
styles,
|
||||
};
|
||||
@@ -1,14 +1,15 @@
|
||||
import flowDb from './flowDb.js';
|
||||
import type { FlowSubGraph } from './types.js';
|
||||
|
||||
describe('flow db subgraphs', () => {
|
||||
let subgraphs;
|
||||
let subgraphs: FlowSubGraph[];
|
||||
beforeEach(() => {
|
||||
subgraphs = [
|
||||
{ nodes: ['a', 'b', 'c', 'e'] },
|
||||
{ nodes: ['f', 'g', 'h'] },
|
||||
{ nodes: ['i', 'j'] },
|
||||
{ nodes: ['k'] },
|
||||
];
|
||||
] as FlowSubGraph[];
|
||||
});
|
||||
describe('exist', () => {
|
||||
it('should return true when the is exists in a subgraph', () => {
|
||||
@@ -25,17 +26,17 @@ describe('flow db subgraphs', () => {
|
||||
|
||||
describe('makeUniq', () => {
|
||||
it('should remove ids from sungraph that already exists in another subgraph even if it gets empty', () => {
|
||||
const subgraph = flowDb.makeUniq({ nodes: ['i', 'j'] }, subgraphs);
|
||||
const subgraph = flowDb.makeUniq({ nodes: ['i', 'j'] } as FlowSubGraph, subgraphs);
|
||||
|
||||
expect(subgraph.nodes).toEqual([]);
|
||||
});
|
||||
it('should remove ids from sungraph that already exists in another subgraph', () => {
|
||||
const subgraph = flowDb.makeUniq({ nodes: ['i', 'j', 'o'] }, subgraphs);
|
||||
const subgraph = flowDb.makeUniq({ nodes: ['i', 'j', 'o'] } as FlowSubGraph, subgraphs);
|
||||
|
||||
expect(subgraph.nodes).toEqual(['o']);
|
||||
});
|
||||
it('should not remove ids from subgraph if they are unique', () => {
|
||||
const subgraph = flowDb.makeUniq({ nodes: ['q', 'r', 's'] }, subgraphs);
|
||||
const subgraph = flowDb.makeUniq({ nodes: ['q', 'r', 's'] } as FlowSubGraph, subgraphs);
|
||||
|
||||
expect(subgraph.nodes).toEqual(['q', 'r', 's']);
|
||||
});
|
||||
@@ -12,38 +12,38 @@ import {
|
||||
setDiagramTitle,
|
||||
getDiagramTitle,
|
||||
} from '../common/commonDb.js';
|
||||
import type { FlowVertex, FlowClass, FlowSubGraph, FlowText, FlowEdge, FlowLink } from './types.js';
|
||||
|
||||
const MERMAID_DOM_ID_PREFIX = 'flowchart-';
|
||||
let vertexCounter = 0;
|
||||
let config = getConfig();
|
||||
let vertices = {};
|
||||
let edges = [];
|
||||
let classes = {};
|
||||
let subGraphs = [];
|
||||
let subGraphLookup = {};
|
||||
let tooltips = {};
|
||||
let vertices: Record<string, FlowVertex> = {};
|
||||
let edges: FlowEdge[] & { defaultInterpolate?: string; defaultStyle?: string[] } = [];
|
||||
let classes: Record<string, FlowClass> = {};
|
||||
let subGraphs: FlowSubGraph[] = [];
|
||||
let subGraphLookup: Record<string, FlowSubGraph> = {};
|
||||
let tooltips: Record<string, string> = {};
|
||||
let subCount = 0;
|
||||
let firstGraphFlag = true;
|
||||
let direction;
|
||||
let direction: string;
|
||||
|
||||
let version; // As in graph
|
||||
let version: string; // As in graph
|
||||
|
||||
// Functions to be run after graph rendering
|
||||
let funs = [];
|
||||
let funs: ((element: Element) => void)[] = []; // cspell:ignore funs
|
||||
|
||||
const sanitizeText = (txt) => common.sanitizeText(txt, config);
|
||||
const sanitizeText = (txt: string) => common.sanitizeText(txt, config);
|
||||
|
||||
/**
|
||||
* Function to lookup domId from id in the graph definition.
|
||||
*
|
||||
* @param id
|
||||
* @public
|
||||
* @param id - id of the node
|
||||
*/
|
||||
export const lookUpDomId = function (id) {
|
||||
const veritceKeys = Object.keys(vertices);
|
||||
for (const veritceKey of veritceKeys) {
|
||||
if (vertices[veritceKey].id === id) {
|
||||
return vertices[veritceKey].domId;
|
||||
export const lookUpDomId = function (id: string) {
|
||||
const vertexKeys = Object.keys(vertices);
|
||||
for (const vertexKey of vertexKeys) {
|
||||
if (vertices[vertexKey].id === id) {
|
||||
return vertices[vertexKey].domId;
|
||||
}
|
||||
}
|
||||
return id;
|
||||
@@ -52,30 +52,24 @@ export const lookUpDomId = function (id) {
|
||||
/**
|
||||
* Function called by parser when a node definition has been found
|
||||
*
|
||||
* @param _id
|
||||
* @param text
|
||||
* @param textObj
|
||||
* @param type
|
||||
* @param style
|
||||
* @param classes
|
||||
* @param dir
|
||||
* @param props
|
||||
*/
|
||||
export const addVertex = function (_id, textObj, type, style, classes, dir, props = {}) {
|
||||
export const addVertex = function (
|
||||
id: string,
|
||||
textObj: FlowText,
|
||||
type: 'group',
|
||||
style: string[],
|
||||
classes: string[],
|
||||
dir: string,
|
||||
props = {}
|
||||
) {
|
||||
if (!id || id.trim().length === 0) {
|
||||
return;
|
||||
}
|
||||
let txt;
|
||||
let id = _id;
|
||||
if (id === undefined) {
|
||||
return;
|
||||
}
|
||||
if (id.trim().length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// if (id[0].match(/\d/)) id = MERMAID_DOM_ID_PREFIX + id;
|
||||
|
||||
if (vertices[id] === undefined) {
|
||||
vertices[id] = {
|
||||
id: id,
|
||||
id,
|
||||
labelType: 'text',
|
||||
domId: MERMAID_DOM_ID_PREFIX + id + '-' + vertexCounter,
|
||||
styles: [],
|
||||
@@ -94,7 +88,7 @@ export const addVertex = function (_id, textObj, type, style, classes, dir, prop
|
||||
vertices[id].text = txt;
|
||||
} else {
|
||||
if (vertices[id].text === undefined) {
|
||||
vertices[id].text = _id;
|
||||
vertices[id].text = id;
|
||||
}
|
||||
}
|
||||
if (type !== undefined) {
|
||||
@@ -123,20 +117,12 @@ export const addVertex = function (_id, textObj, type, style, classes, dir, prop
|
||||
/**
|
||||
* Function called by parser when a link/edge definition has been found
|
||||
*
|
||||
* @param _start
|
||||
* @param _end
|
||||
* @param type
|
||||
* @param linkText
|
||||
* @param linkTextObj
|
||||
*/
|
||||
export const addSingleLink = function (_start, _end, type) {
|
||||
let start = _start;
|
||||
let end = _end;
|
||||
// if (start[0].match(/\d/)) start = MERMAID_DOM_ID_PREFIX + start;
|
||||
// if (end[0].match(/\d/)) end = MERMAID_DOM_ID_PREFIX + end;
|
||||
// log.info('Got edge...', start, end);
|
||||
export const addSingleLink = function (_start: string, _end: string, type: any) {
|
||||
const start = _start;
|
||||
const end = _end;
|
||||
|
||||
const edge = { start: start, end: end, type: undefined, text: '', labelType: 'text' };
|
||||
const edge: FlowEdge = { start: start, end: end, type: undefined, text: '', labelType: 'text' };
|
||||
log.info('abc78 Got edge...', edge);
|
||||
const linkTextObj = type.text;
|
||||
|
||||
@@ -153,30 +139,28 @@ export const addSingleLink = function (_start, _end, type) {
|
||||
if (type !== undefined) {
|
||||
edge.type = type.type;
|
||||
edge.stroke = type.stroke;
|
||||
edge.length = type.length;
|
||||
}
|
||||
if (edge?.length > 10) {
|
||||
edge.length = 10;
|
||||
edge.length = type.length > 10 ? 10 : type.length;
|
||||
}
|
||||
|
||||
if (edges.length < (config.maxEdges ?? 500)) {
|
||||
log.info('abc78 pushing edge...');
|
||||
log.info('Pushing edge...');
|
||||
edges.push(edge);
|
||||
} else {
|
||||
throw new Error(
|
||||
`Edge limit exceeded. ${edges.length} edges found, but the limit is ${config.maxEdges}.
|
||||
|
||||
Initialize mermaid with maxEdges set to a higher number to allow more edges.
|
||||
You cannot set this config via configuration inside the diagram as it is a secure config.
|
||||
Initialize mermaid with maxEdges set to a higher number to allow more edges.
|
||||
You cannot set this config via configuration inside the diagram as it is a secure config.
|
||||
You have to call mermaid.initialize.`
|
||||
);
|
||||
}
|
||||
};
|
||||
export const addLink = function (_start, _end, type) {
|
||||
log.info('addLink (abc78)', _start, _end, type);
|
||||
let i, j;
|
||||
for (i = 0; i < _start.length; i++) {
|
||||
for (j = 0; j < _end.length; j++) {
|
||||
addSingleLink(_start[i], _end[j], type);
|
||||
|
||||
export const addLink = function (_start: string[], _end: string[], type: unknown) {
|
||||
log.info('addLink', _start, _end, type);
|
||||
for (const start of _start) {
|
||||
for (const end of _end) {
|
||||
addSingleLink(start, end, type);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -184,15 +168,16 @@ export const addLink = function (_start, _end, type) {
|
||||
/**
|
||||
* Updates a link's line interpolation algorithm
|
||||
*
|
||||
* @param positions
|
||||
* @param interp
|
||||
*/
|
||||
export const updateLinkInterpolate = function (positions, interp) {
|
||||
export const updateLinkInterpolate = function (
|
||||
positions: ('default' | number)[],
|
||||
interpolate: string
|
||||
) {
|
||||
positions.forEach(function (pos) {
|
||||
if (pos === 'default') {
|
||||
edges.defaultInterpolate = interp;
|
||||
edges.defaultInterpolate = interpolate;
|
||||
} else {
|
||||
edges[pos].interpolate = interp;
|
||||
edges[pos].interpolate = interpolate;
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -200,12 +185,10 @@ export const updateLinkInterpolate = function (positions, interp) {
|
||||
/**
|
||||
* Updates a link with a style
|
||||
*
|
||||
* @param positions
|
||||
* @param style
|
||||
*/
|
||||
export const updateLink = function (positions, style) {
|
||||
export const updateLink = function (positions: ('default' | number)[], style: string[]) {
|
||||
positions.forEach(function (pos) {
|
||||
if (pos >= edges.length) {
|
||||
if (typeof pos === 'number' && pos >= edges.length) {
|
||||
throw new Error(
|
||||
`The index ${pos} for linkStyle is out of bounds. Valid indices for linkStyle are between 0 and ${
|
||||
edges.length - 1
|
||||
@@ -223,7 +206,7 @@ export const updateLink = function (positions, style) {
|
||||
});
|
||||
};
|
||||
|
||||
export const addClass = function (ids, style) {
|
||||
export const addClass = function (ids: string, style: string[]) {
|
||||
ids.split(',').forEach(function (id) {
|
||||
if (classes[id] === undefined) {
|
||||
classes[id] = { id, styles: [], textStyles: [] };
|
||||
@@ -244,9 +227,8 @@ export const addClass = function (ids, style) {
|
||||
/**
|
||||
* Called by parser when a graph definition is found, stores the direction of the chart.
|
||||
*
|
||||
* @param dir
|
||||
*/
|
||||
export const setDirection = function (dir) {
|
||||
export const setDirection = function (dir: string) {
|
||||
direction = dir;
|
||||
if (direction.match(/.*</)) {
|
||||
direction = 'RL';
|
||||
@@ -268,34 +250,32 @@ export const setDirection = function (dir) {
|
||||
/**
|
||||
* Called by parser when a special node is found, e.g. a clickable element.
|
||||
*
|
||||
* @param ids Comma separated list of ids
|
||||
* @param className Class to add
|
||||
* @param ids - Comma separated list of ids
|
||||
* @param className - Class to add
|
||||
*/
|
||||
export const setClass = function (ids, className) {
|
||||
ids.split(',').forEach(function (_id) {
|
||||
// let id = version === 'gen-2' ? lookUpDomId(_id) : _id;
|
||||
let id = _id;
|
||||
// if (_id[0].match(/\d/)) id = MERMAID_DOM_ID_PREFIX + id;
|
||||
if (vertices[id] !== undefined) {
|
||||
export const setClass = function (ids: string, className: string) {
|
||||
for (const id of ids.split(',')) {
|
||||
if (vertices[id]) {
|
||||
vertices[id].classes.push(className);
|
||||
}
|
||||
|
||||
if (subGraphLookup[id] !== undefined) {
|
||||
if (subGraphLookup[id]) {
|
||||
subGraphLookup[id].classes.push(className);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const setTooltip = function (ids, tooltip) {
|
||||
ids.split(',').forEach(function (id) {
|
||||
if (tooltip !== undefined) {
|
||||
tooltips[version === 'gen-1' ? lookUpDomId(id) : id] = sanitizeText(tooltip);
|
||||
}
|
||||
});
|
||||
const setTooltip = function (ids: string, tooltip: string) {
|
||||
if (tooltip === undefined) {
|
||||
return;
|
||||
}
|
||||
tooltip = sanitizeText(tooltip);
|
||||
for (const id of ids.split(',')) {
|
||||
tooltips[version === 'gen-1' ? lookUpDomId(id) : id] = tooltip;
|
||||
}
|
||||
};
|
||||
|
||||
const setClickFun = function (id, functionName, functionArgs) {
|
||||
let domId = lookUpDomId(id);
|
||||
const setClickFun = function (id: string, functionName: string, functionArgs: string) {
|
||||
const domId = lookUpDomId(id);
|
||||
// if (_id[0].match(/\d/)) id = MERMAID_DOM_ID_PREFIX + id;
|
||||
if (getConfig().securityLevel !== 'loose') {
|
||||
return;
|
||||
@@ -303,7 +283,7 @@ const setClickFun = function (id, functionName, functionArgs) {
|
||||
if (functionName === undefined) {
|
||||
return;
|
||||
}
|
||||
let argList = [];
|
||||
let argList: string[] = [];
|
||||
if (typeof functionArgs === 'string') {
|
||||
/* Splits functionArgs by ',', ignoring all ',' in double quoted strings */
|
||||
argList = functionArgs.split(/,(?=(?:(?:[^"]*"){2})*[^"]*$)/);
|
||||
@@ -343,11 +323,11 @@ const setClickFun = function (id, functionName, functionArgs) {
|
||||
/**
|
||||
* Called by parser when a link is found. Adds the URL to the vertex data.
|
||||
*
|
||||
* @param ids Comma separated list of ids
|
||||
* @param linkStr URL to create a link for
|
||||
* @param target
|
||||
* @param ids - Comma separated list of ids
|
||||
* @param linkStr - URL to create a link for
|
||||
* @param target - Target attribute for the link
|
||||
*/
|
||||
export const setLink = function (ids, linkStr, target) {
|
||||
export const setLink = function (ids: string, linkStr: string, target: string) {
|
||||
ids.split(',').forEach(function (id) {
|
||||
if (vertices[id] !== undefined) {
|
||||
vertices[id].link = utils.formatUrl(linkStr, config);
|
||||
@@ -356,7 +336,8 @@ export const setLink = function (ids, linkStr, target) {
|
||||
});
|
||||
setClass(ids, 'clickable');
|
||||
};
|
||||
export const getTooltip = function (id) {
|
||||
|
||||
export const getTooltip = function (id: string) {
|
||||
if (tooltips.hasOwnProperty(id)) {
|
||||
return tooltips[id];
|
||||
}
|
||||
@@ -366,18 +347,18 @@ export const getTooltip = function (id) {
|
||||
/**
|
||||
* Called by parser when a click definition is found. Registers an event handler.
|
||||
*
|
||||
* @param ids Comma separated list of ids
|
||||
* @param functionName Function to be called on click
|
||||
* @param functionArgs
|
||||
* @param ids - Comma separated list of ids
|
||||
* @param functionName - Function to be called on click
|
||||
* @param functionArgs - Arguments to be passed to the function
|
||||
*/
|
||||
export const setClickEvent = function (ids, functionName, functionArgs) {
|
||||
export const setClickEvent = function (ids: string, functionName: string, functionArgs: string) {
|
||||
ids.split(',').forEach(function (id) {
|
||||
setClickFun(id, functionName, functionArgs);
|
||||
});
|
||||
setClass(ids, 'clickable');
|
||||
};
|
||||
|
||||
export const bindFunctions = function (element) {
|
||||
export const bindFunctions = function (element: Element) {
|
||||
funs.forEach(function (fun) {
|
||||
fun(element);
|
||||
});
|
||||
@@ -388,7 +369,6 @@ export const getDirection = function () {
|
||||
/**
|
||||
* Retrieval function for fetching the found nodes after parsing has completed.
|
||||
*
|
||||
* @returns {{} | any | vertices}
|
||||
*/
|
||||
export const getVertices = function () {
|
||||
return vertices;
|
||||
@@ -397,7 +377,6 @@ export const getVertices = function () {
|
||||
/**
|
||||
* Retrieval function for fetching the found links after parsing has completed.
|
||||
*
|
||||
* @returns {{} | any | edges}
|
||||
*/
|
||||
export const getEdges = function () {
|
||||
return edges;
|
||||
@@ -406,15 +385,16 @@ export const getEdges = function () {
|
||||
/**
|
||||
* Retrieval function for fetching the found class definitions after parsing has completed.
|
||||
*
|
||||
* @returns {{} | any | classes}
|
||||
*/
|
||||
export const getClasses = function () {
|
||||
return classes;
|
||||
};
|
||||
|
||||
const setupToolTips = function (element) {
|
||||
const setupToolTips = function (element: Element) {
|
||||
let tooltipElem = select('.mermaidTooltip');
|
||||
// @ts-ignore TODO: fix this
|
||||
if ((tooltipElem._groups || tooltipElem)[0][0] === null) {
|
||||
// @ts-ignore TODO: fix this
|
||||
tooltipElem = select('body').append('div').attr('class', 'mermaidTooltip').style('opacity', 0);
|
||||
}
|
||||
|
||||
@@ -426,12 +406,13 @@ const setupToolTips = function (element) {
|
||||
const el = select(this);
|
||||
const title = el.attr('title');
|
||||
|
||||
// Dont try to draw a tooltip if no data is provided
|
||||
// Don't try to draw a tooltip if no data is provided
|
||||
if (title === null) {
|
||||
return;
|
||||
}
|
||||
const rect = this.getBoundingClientRect();
|
||||
const rect = (this as Element)?.getBoundingClientRect();
|
||||
|
||||
// @ts-ignore TODO: fix this
|
||||
tooltipElem.transition().duration(200).style('opacity', '.9');
|
||||
tooltipElem
|
||||
.text(el.attr('title'))
|
||||
@@ -441,6 +422,7 @@ const setupToolTips = function (element) {
|
||||
el.classed('hover', true);
|
||||
})
|
||||
.on('mouseout', function () {
|
||||
// @ts-ignore TODO: fix this
|
||||
tooltipElem.transition().duration(500).style('opacity', 0);
|
||||
const el = select(this);
|
||||
el.classed('hover', false);
|
||||
@@ -451,7 +433,6 @@ funs.push(setupToolTips);
|
||||
/**
|
||||
* Clears the internal graph db so that a new graph can be parsed.
|
||||
*
|
||||
* @param ver
|
||||
*/
|
||||
export const clear = function (ver = 'gen-1') {
|
||||
vertices = {};
|
||||
@@ -467,31 +448,29 @@ export const clear = function (ver = 'gen-1') {
|
||||
config = getConfig();
|
||||
commonClear();
|
||||
};
|
||||
export const setGen = (ver) => {
|
||||
|
||||
export const setGen = (ver: string) => {
|
||||
version = ver || 'gen-2';
|
||||
};
|
||||
/** @returns {string} */
|
||||
|
||||
export const defaultStyle = function () {
|
||||
return 'fill:#ffa;stroke: #f66; stroke-width: 3px; stroke-dasharray: 5, 5;fill:#ffa;stroke: #666;';
|
||||
};
|
||||
|
||||
/**
|
||||
* Clears the internal graph db so that a new graph can be parsed.
|
||||
*
|
||||
* @param _id
|
||||
* @param list
|
||||
* @param _title
|
||||
*/
|
||||
export const addSubGraph = function (_id, list, _title) {
|
||||
let id = _id.text.trim();
|
||||
export const addSubGraph = function (
|
||||
_id: { text: string },
|
||||
list: string[],
|
||||
_title: { text: string; type: string }
|
||||
) {
|
||||
let id: string | undefined = _id.text.trim();
|
||||
let title = _title.text;
|
||||
if (_id === _title && _title.text.match(/\s/)) {
|
||||
id = undefined;
|
||||
}
|
||||
/** @param a */
|
||||
function uniq(a) {
|
||||
const prims = { boolean: {}, number: {}, string: {} };
|
||||
const objs = [];
|
||||
|
||||
function uniq(a: any[]) {
|
||||
const prims: any = { boolean: {}, number: {}, string: {} };
|
||||
const objs: any[] = [];
|
||||
|
||||
let dir; // = undefined; direction.trim();
|
||||
const nodeList = a.filter(function (item) {
|
||||
@@ -512,10 +491,7 @@ export const addSubGraph = function (_id, list, _title) {
|
||||
return { nodeList, dir };
|
||||
}
|
||||
|
||||
let nodeList = [];
|
||||
|
||||
const { nodeList: nl, dir } = uniq(nodeList.concat.apply(nodeList, list));
|
||||
nodeList = nl;
|
||||
const { nodeList, dir } = uniq(list.flat());
|
||||
if (version === 'gen-1') {
|
||||
for (let i = 0; i < nodeList.length; i++) {
|
||||
nodeList[i] = lookUpDomId(nodeList[i]);
|
||||
@@ -523,7 +499,6 @@ export const addSubGraph = function (_id, list, _title) {
|
||||
}
|
||||
|
||||
id = id || 'subGraph' + subCount;
|
||||
// if (id[0].match(/\d/)) id = lookUpDomId(id);
|
||||
title = title || '';
|
||||
title = sanitizeText(title);
|
||||
subCount = subCount + 1;
|
||||
@@ -538,19 +513,6 @@ export const addSubGraph = function (_id, list, _title) {
|
||||
|
||||
log.info('Adding', subGraph.id, subGraph.nodes, subGraph.dir);
|
||||
|
||||
/** Deletes an id from all subgraphs */
|
||||
// const del = _id => {
|
||||
// subGraphs.forEach(sg => {
|
||||
// const pos = sg.nodes.indexOf(_id);
|
||||
// if (pos >= 0) {
|
||||
// sg.nodes.splice(pos, 1);
|
||||
// }
|
||||
// });
|
||||
// };
|
||||
|
||||
// // Removes the members of this subgraph from any other subgraphs, a node only belong to one subgraph
|
||||
// subGraph.nodes.forEach(_id => del(_id));
|
||||
|
||||
// Remove the members in the new subgraph if they already belong to another subgraph
|
||||
subGraph.nodes = makeUniq(subGraph, subGraphs).nodes;
|
||||
subGraphs.push(subGraph);
|
||||
@@ -558,7 +520,7 @@ export const addSubGraph = function (_id, list, _title) {
|
||||
return id;
|
||||
};
|
||||
|
||||
const getPosForId = function (id) {
|
||||
const getPosForId = function (id: string) {
|
||||
for (const [i, subGraph] of subGraphs.entries()) {
|
||||
if (subGraph.id === id) {
|
||||
return i;
|
||||
@@ -567,12 +529,15 @@ const getPosForId = function (id) {
|
||||
return -1;
|
||||
};
|
||||
let secCount = -1;
|
||||
const posCrossRef = [];
|
||||
const indexNodes2 = function (id, pos) {
|
||||
const posCrossRef: number[] = [];
|
||||
const indexNodes2 = function (id: string, pos: number): { result: boolean; count: number } {
|
||||
const nodes = subGraphs[pos].nodes;
|
||||
secCount = secCount + 1;
|
||||
if (secCount > 2000) {
|
||||
return;
|
||||
return {
|
||||
result: false,
|
||||
count: 0,
|
||||
};
|
||||
}
|
||||
posCrossRef[secCount] = pos;
|
||||
// Check if match
|
||||
@@ -608,13 +573,13 @@ const indexNodes2 = function (id, pos) {
|
||||
};
|
||||
};
|
||||
|
||||
export const getDepthFirstPos = function (pos) {
|
||||
export const getDepthFirstPos = function (pos: number) {
|
||||
return posCrossRef[pos];
|
||||
};
|
||||
export const indexNodes = function () {
|
||||
secCount = -1;
|
||||
if (subGraphs.length > 0) {
|
||||
indexNodes2('none', subGraphs.length - 1, 0);
|
||||
indexNodes2('none', subGraphs.length - 1);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -630,7 +595,7 @@ export const firstGraph = () => {
|
||||
return false;
|
||||
};
|
||||
|
||||
const destructStartLink = (_str) => {
|
||||
const destructStartLink = (_str: string): FlowLink => {
|
||||
let str = _str.trim();
|
||||
let type = 'arrow_open';
|
||||
|
||||
@@ -662,7 +627,7 @@ const destructStartLink = (_str) => {
|
||||
return { type, stroke };
|
||||
};
|
||||
|
||||
const countChar = (char, str) => {
|
||||
const countChar = (char: string, str: string) => {
|
||||
const length = str.length;
|
||||
let count = 0;
|
||||
for (let i = 0; i < length; ++i) {
|
||||
@@ -673,7 +638,7 @@ const countChar = (char, str) => {
|
||||
return count;
|
||||
};
|
||||
|
||||
const destructEndLink = (_str) => {
|
||||
const destructEndLink = (_str: string) => {
|
||||
const str = _str.trim();
|
||||
let line = str.slice(0, -1);
|
||||
let type = 'arrow_open';
|
||||
@@ -713,7 +678,7 @@ const destructEndLink = (_str) => {
|
||||
stroke = 'invisible';
|
||||
}
|
||||
|
||||
let dots = countChar('.', line);
|
||||
const dots = countChar('.', line);
|
||||
|
||||
if (dots) {
|
||||
stroke = 'dotted';
|
||||
@@ -723,7 +688,7 @@ const destructEndLink = (_str) => {
|
||||
return { type, stroke, length };
|
||||
};
|
||||
|
||||
export const destructLink = (_str, _startStr) => {
|
||||
export const destructLink = (_str: string, _startStr: string) => {
|
||||
const info = destructEndLink(_str);
|
||||
let startInfo;
|
||||
if (_startStr) {
|
||||
@@ -757,7 +722,7 @@ export const destructLink = (_str, _startStr) => {
|
||||
};
|
||||
|
||||
// Todo optimizer this by caching existing nodes
|
||||
const exists = (allSgs, _id) => {
|
||||
const exists = (allSgs: FlowSubGraph[], _id: string) => {
|
||||
let res = false;
|
||||
allSgs.forEach((sg) => {
|
||||
const pos = sg.nodes.indexOf(_id);
|
||||
@@ -770,11 +735,9 @@ const exists = (allSgs, _id) => {
|
||||
/**
|
||||
* Deletes an id from all subgraphs
|
||||
*
|
||||
* @param sg
|
||||
* @param allSubgraphs
|
||||
*/
|
||||
const makeUniq = (sg, allSubgraphs) => {
|
||||
const res = [];
|
||||
const makeUniq = (sg: FlowSubGraph, allSubgraphs: FlowSubGraph[]) => {
|
||||
const res: string[] = [];
|
||||
sg.nodes.forEach((_id, pos) => {
|
||||
if (!exists(allSubgraphs, _id)) {
|
||||
res.push(sg.nodes[pos]);
|
||||
@@ -786,6 +749,7 @@ const makeUniq = (sg, allSubgraphs) => {
|
||||
export const lex = {
|
||||
firstGraph,
|
||||
};
|
||||
|
||||
export default {
|
||||
defaultConfig: () => defaultConfig.flowchart,
|
||||
setAccTitle,
|
||||
@@ -5,7 +5,7 @@ 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';
|
||||
import common, { evaluate } from '../common/common.js';
|
||||
import common, { evaluate, renderKatex } from '../common/common.js';
|
||||
import { interpolateToCurve, getStylesFromArray } from '../../utils.js';
|
||||
import { setupGraphViewbox } from '../../setupGraphViewbox.js';
|
||||
|
||||
@@ -27,12 +27,12 @@ export const setConf = function (cnf) {
|
||||
* @param doc
|
||||
* @param diagObj
|
||||
*/
|
||||
export const addVertices = function (vert, g, svgId, root, doc, diagObj) {
|
||||
export const addVertices = async function (vert, g, svgId, root, doc, diagObj) {
|
||||
const svg = root.select(`[id="${svgId}"]`);
|
||||
const keys = Object.keys(vert);
|
||||
|
||||
// Iterate through each item in the vertex object (containing all the vertices found) in the graph definition
|
||||
keys.forEach(function (id) {
|
||||
for (const id of keys) {
|
||||
const vertex = vert[id];
|
||||
|
||||
/**
|
||||
@@ -59,10 +59,7 @@ export const addVertices = function (vert, g, svgId, root, doc, diagObj) {
|
||||
if (evaluate(getConfig().flowchart.htmlLabels)) {
|
||||
// TODO: addHtmlLabel accepts a labelStyle. Do we possibly have that?
|
||||
const node = {
|
||||
label: vertexText.replace(
|
||||
/fa[blrs]?:fa-[\w-]+/g,
|
||||
(s) => `<i class='${s.replace(':', ' ')}'></i>`
|
||||
),
|
||||
label: vertexText,
|
||||
};
|
||||
vertexNode = addHtmlLabel(svg, node).node();
|
||||
vertexNode.parentNode.removeChild(vertexNode);
|
||||
@@ -84,12 +81,12 @@ export const addVertices = function (vert, g, svgId, root, doc, diagObj) {
|
||||
}
|
||||
}
|
||||
|
||||
let radious = 0;
|
||||
let radius = 0;
|
||||
let _shape = '';
|
||||
// Set the shape based parameters
|
||||
switch (vertex.type) {
|
||||
case 'round':
|
||||
radious = 5;
|
||||
radius = 5;
|
||||
_shape = 'rect';
|
||||
break;
|
||||
case 'square':
|
||||
@@ -143,14 +140,16 @@ export const addVertices = function (vert, g, svgId, root, doc, diagObj) {
|
||||
default:
|
||||
_shape = 'rect';
|
||||
}
|
||||
const labelText = await renderKatex(vertexText, getConfig());
|
||||
|
||||
// Add the node
|
||||
g.setNode(vertex.id, {
|
||||
labelStyle: styles.labelStyle,
|
||||
shape: _shape,
|
||||
labelText: vertexText,
|
||||
labelText,
|
||||
labelType: vertex.labelType,
|
||||
rx: radious,
|
||||
ry: radious,
|
||||
rx: radius,
|
||||
ry: radius,
|
||||
class: classStr,
|
||||
style: styles.style,
|
||||
id: vertex.id,
|
||||
@@ -170,9 +169,9 @@ export const addVertices = function (vert, g, svgId, root, doc, diagObj) {
|
||||
labelStyle: styles.labelStyle,
|
||||
labelType: vertex.labelType,
|
||||
shape: _shape,
|
||||
labelText: vertexText,
|
||||
rx: radious,
|
||||
ry: radious,
|
||||
labelText,
|
||||
rx: radius,
|
||||
ry: radius,
|
||||
class: classStr,
|
||||
style: styles.style,
|
||||
id: vertex.id,
|
||||
@@ -183,7 +182,7 @@ export const addVertices = function (vert, g, svgId, root, doc, diagObj) {
|
||||
props: vertex.props,
|
||||
padding: getConfig().flowchart.padding,
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -193,7 +192,7 @@ export const addVertices = function (vert, g, svgId, root, doc, diagObj) {
|
||||
* @param {object} g The graph object
|
||||
* @param diagObj
|
||||
*/
|
||||
export const addEdges = function (edges, g, diagObj) {
|
||||
export const addEdges = async function (edges, g, diagObj) {
|
||||
log.info('abc78 edges = ', edges);
|
||||
let cnt = 0;
|
||||
let linkIdCnt = {};
|
||||
@@ -207,7 +206,7 @@ export const addEdges = function (edges, g, diagObj) {
|
||||
defaultLabelStyle = defaultStyles.labelStyle;
|
||||
}
|
||||
|
||||
edges.forEach(function (edge) {
|
||||
for (const edge of edges) {
|
||||
cnt++;
|
||||
|
||||
// Identify Link
|
||||
@@ -315,9 +314,8 @@ export const addEdges = function (edges, g, diagObj) {
|
||||
edgeData.arrowheadStyle = 'fill: #333';
|
||||
edgeData.labelpos = 'c';
|
||||
}
|
||||
|
||||
edgeData.labelType = edge.labelType;
|
||||
edgeData.label = edge.text.replace(common.lineBreakRegex, '\n');
|
||||
edgeData.label = await renderKatex(edge.text.replace(common.lineBreakRegex, '\n'), getConfig());
|
||||
|
||||
if (edge.style === undefined) {
|
||||
edgeData.style = edgeData.style || 'stroke: #333; stroke-width: 1.5px;fill:none;';
|
||||
@@ -330,7 +328,7 @@ export const addEdges = function (edges, g, diagObj) {
|
||||
|
||||
// Add the edge to the graph
|
||||
g.setEdge(edge.start, edge.end, edgeData, cnt);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -349,6 +347,8 @@ export const getClasses = function (text, diagObj) {
|
||||
*
|
||||
* @param text
|
||||
* @param id
|
||||
* @param _version
|
||||
* @param diagObj
|
||||
*/
|
||||
|
||||
export const draw = async function (text, id, _version, diagObj) {
|
||||
@@ -425,8 +425,8 @@ export const draw = async function (text, id, _version, diagObj) {
|
||||
g.setParent(subG.nodes[j], subG.id);
|
||||
}
|
||||
}
|
||||
addVertices(vert, g, id, root, doc, diagObj);
|
||||
addEdges(edges, g, diagObj);
|
||||
await addVertices(vert, g, id, root, doc, diagObj);
|
||||
await addEdges(edges, g, diagObj);
|
||||
|
||||
// Add custom shapes
|
||||
// flowChartShapes.addToRenderV2(addShape);
|
||||
|
||||
@@ -15,7 +15,7 @@ describe('when using mermaid and ', function () {
|
||||
flowDb.clear();
|
||||
flowDb.setGen('gen-2');
|
||||
});
|
||||
it('should handle edges with text', () => {
|
||||
it('should handle edges with text', async () => {
|
||||
parser.parse('graph TD;A-->|text ex|B;');
|
||||
flowDb.getVertices();
|
||||
const edges = flowDb.getEdges();
|
||||
@@ -29,7 +29,7 @@ describe('when using mermaid and ', function () {
|
||||
},
|
||||
};
|
||||
|
||||
flowRenderer.addEdges(edges, mockG, diag);
|
||||
await flowRenderer.addEdges(edges, mockG, diag);
|
||||
});
|
||||
|
||||
it('should handle edges without text', async function () {
|
||||
@@ -45,10 +45,10 @@ describe('when using mermaid and ', function () {
|
||||
},
|
||||
};
|
||||
|
||||
flowRenderer.addEdges(edges, mockG, diag);
|
||||
await flowRenderer.addEdges(edges, mockG, diag);
|
||||
});
|
||||
|
||||
it('should handle open-ended edges', () => {
|
||||
it('should handle open-ended edges', async () => {
|
||||
parser.parse('graph TD;A---B;');
|
||||
flowDb.getVertices();
|
||||
const edges = flowDb.getEdges();
|
||||
@@ -61,10 +61,10 @@ describe('when using mermaid and ', function () {
|
||||
},
|
||||
};
|
||||
|
||||
flowRenderer.addEdges(edges, mockG, diag);
|
||||
await flowRenderer.addEdges(edges, mockG, diag);
|
||||
});
|
||||
|
||||
it('should handle edges with styles defined', () => {
|
||||
it('should handle edges with styles defined', async () => {
|
||||
parser.parse('graph TD;A---B; linkStyle 0 stroke:val1,stroke-width:val2;');
|
||||
flowDb.getVertices();
|
||||
const edges = flowDb.getEdges();
|
||||
@@ -78,9 +78,9 @@ describe('when using mermaid and ', function () {
|
||||
},
|
||||
};
|
||||
|
||||
flowRenderer.addEdges(edges, mockG, diag);
|
||||
await flowRenderer.addEdges(edges, mockG, diag);
|
||||
});
|
||||
it('should handle edges with interpolation defined', () => {
|
||||
it('should handle edges with interpolation defined', async () => {
|
||||
parser.parse('graph TD;A---B; linkStyle 0 interpolate basis');
|
||||
flowDb.getVertices();
|
||||
const edges = flowDb.getEdges();
|
||||
@@ -94,9 +94,9 @@ describe('when using mermaid and ', function () {
|
||||
},
|
||||
};
|
||||
|
||||
flowRenderer.addEdges(edges, mockG, diag);
|
||||
await flowRenderer.addEdges(edges, mockG, diag);
|
||||
});
|
||||
it('should handle edges with text and styles defined', () => {
|
||||
it('should handle edges with text and styles defined', async () => {
|
||||
parser.parse('graph TD;A---|the text|B; linkStyle 0 stroke:val1,stroke-width:val2;');
|
||||
flowDb.getVertices();
|
||||
const edges = flowDb.getEdges();
|
||||
@@ -111,10 +111,10 @@ describe('when using mermaid and ', function () {
|
||||
},
|
||||
};
|
||||
|
||||
flowRenderer.addEdges(edges, mockG, diag);
|
||||
await flowRenderer.addEdges(edges, mockG, diag);
|
||||
});
|
||||
|
||||
it('should set fill to "none" by default when handling edges', () => {
|
||||
it('should set fill to "none" by default when handling edges', async () => {
|
||||
parser.parse('graph TD;A---B; linkStyle 0 stroke:val1,stroke-width:val2;');
|
||||
flowDb.getVertices();
|
||||
const edges = flowDb.getEdges();
|
||||
@@ -128,10 +128,10 @@ describe('when using mermaid and ', function () {
|
||||
},
|
||||
};
|
||||
|
||||
flowRenderer.addEdges(edges, mockG, diag);
|
||||
await flowRenderer.addEdges(edges, mockG, diag);
|
||||
});
|
||||
|
||||
it('should not set fill to none if fill is set in linkStyle', () => {
|
||||
it('should not set fill to none if fill is set in linkStyle', async () => {
|
||||
parser.parse('graph TD;A---B; linkStyle 0 stroke:val1,stroke-width:val2,fill:blue;');
|
||||
flowDb.getVertices();
|
||||
const edges = flowDb.getEdges();
|
||||
@@ -144,7 +144,7 @@ describe('when using mermaid and ', function () {
|
||||
},
|
||||
};
|
||||
|
||||
flowRenderer.addEdges(edges, mockG, diag);
|
||||
await flowRenderer.addEdges(edges, mockG, diag);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@ import { render as Render } from 'dagre-d3-es';
|
||||
import { applyStyle } from 'dagre-d3-es/src/dagre-js/util.js';
|
||||
import { addHtmlLabel } from 'dagre-d3-es/src/dagre-js/label/add-html-label.js';
|
||||
import { log } from '../../logger.js';
|
||||
import common, { evaluate } from '../common/common.js';
|
||||
import common, { evaluate, renderKatex } from '../common/common.js';
|
||||
import { interpolateToCurve, getStylesFromArray } from '../../utils.js';
|
||||
import { setupGraphViewbox } from '../../setupGraphViewbox.js';
|
||||
import flowChartShapes from './flowChartShapes.js';
|
||||
@@ -28,13 +28,13 @@ export const setConf = function (cnf) {
|
||||
* @param _doc
|
||||
* @param diagObj
|
||||
*/
|
||||
export const addVertices = function (vert, g, svgId, root, _doc, diagObj) {
|
||||
export const addVertices = async function (vert, g, svgId, root, _doc, diagObj) {
|
||||
const svg = !root ? select(`[id="${svgId}"]`) : root.select(`[id="${svgId}"]`);
|
||||
const doc = !_doc ? document : _doc;
|
||||
const keys = Object.keys(vert);
|
||||
|
||||
// Iterate through each item in the vertex object (containing all the vertices found) in the graph definition
|
||||
keys.forEach(function (id) {
|
||||
for (const id of keys) {
|
||||
const vertex = vert[id];
|
||||
|
||||
/**
|
||||
@@ -57,9 +57,12 @@ export const addVertices = function (vert, g, svgId, root, _doc, diagObj) {
|
||||
if (evaluate(getConfig().flowchart.htmlLabels)) {
|
||||
// TODO: addHtmlLabel accepts a labelStyle. Do we possibly have that?
|
||||
const node = {
|
||||
label: vertexText.replace(
|
||||
/fa[blrs]?:fa-[\w-]+/g,
|
||||
(s) => `<i class='${s.replace(':', ' ')}'></i>`
|
||||
label: await renderKatex(
|
||||
vertexText.replace(
|
||||
/fa[blrs]?:fa-[\w-]+/g, // cspell:disable-line
|
||||
(s) => `<i class='${s.replace(':', ' ')}'></i>`
|
||||
),
|
||||
getConfig()
|
||||
),
|
||||
};
|
||||
vertexNode = addHtmlLabel(svg, node).node();
|
||||
@@ -81,12 +84,12 @@ export const addVertices = function (vert, g, svgId, root, _doc, diagObj) {
|
||||
vertexNode = svgLabel;
|
||||
}
|
||||
|
||||
let radious = 0;
|
||||
let radius = 0;
|
||||
let _shape = '';
|
||||
// Set the shape based parameters
|
||||
switch (vertex.type) {
|
||||
case 'round':
|
||||
radious = 5;
|
||||
radius = 5;
|
||||
_shape = 'rect';
|
||||
break;
|
||||
case 'square':
|
||||
@@ -144,13 +147,13 @@ export const addVertices = function (vert, g, svgId, root, _doc, diagObj) {
|
||||
labelStyle: styles.labelStyle,
|
||||
shape: _shape,
|
||||
label: vertexNode,
|
||||
rx: radious,
|
||||
ry: radious,
|
||||
rx: radius,
|
||||
ry: radius,
|
||||
class: classStr,
|
||||
style: styles.style,
|
||||
id: diagObj.db.lookUpDomId(vertex.id),
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -160,7 +163,7 @@ export const addVertices = function (vert, g, svgId, root, _doc, diagObj) {
|
||||
* @param {object} g The graph object
|
||||
* @param diagObj
|
||||
*/
|
||||
export const addEdges = function (edges, g, diagObj) {
|
||||
export const addEdges = async function (edges, g, diagObj) {
|
||||
let cnt = 0;
|
||||
|
||||
let defaultStyle;
|
||||
@@ -172,7 +175,7 @@ export const addEdges = function (edges, g, diagObj) {
|
||||
defaultLabelStyle = defaultStyles.labelStyle;
|
||||
}
|
||||
|
||||
edges.forEach(function (edge) {
|
||||
for (const edge of edges) {
|
||||
cnt++;
|
||||
|
||||
// Identify Link
|
||||
@@ -239,9 +242,12 @@ export const addEdges = function (edges, g, diagObj) {
|
||||
edgeData.labelType = 'html';
|
||||
edgeData.label = `<span id="L-${linkId}" class="edgeLabel L-${linkNameStart}' L-${linkNameEnd}" style="${
|
||||
edgeData.labelStyle
|
||||
}">${edge.text.replace(
|
||||
/fa[blrs]?:fa-[\w-]+/g,
|
||||
(s) => `<i class='${s.replace(':', ' ')}'></i>`
|
||||
}">${await renderKatex(
|
||||
edge.text.replace(
|
||||
/fa[blrs]?:fa-[\w-]+/g, // cspell:disable-line
|
||||
(s) => `<i class='${s.replace(':', ' ')}'></i>`
|
||||
),
|
||||
getConfig()
|
||||
)}</span>`;
|
||||
} else {
|
||||
edgeData.labelType = 'text';
|
||||
@@ -261,7 +267,7 @@ export const addEdges = function (edges, g, diagObj) {
|
||||
|
||||
// Add the edge to the graph
|
||||
g.setEdge(diagObj.db.lookUpDomId(edge.start), diagObj.db.lookUpDomId(edge.end), edgeData, cnt);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -284,7 +290,7 @@ export const getClasses = function (text, diagObj) {
|
||||
* @param _version
|
||||
* @param diagObj
|
||||
*/
|
||||
export const draw = function (text, id, _version, diagObj) {
|
||||
export const draw = async function (text, id, _version, diagObj) {
|
||||
log.info('Drawing flowchart');
|
||||
const { securityLevel, flowchart: conf } = getConfig();
|
||||
let sandboxElement;
|
||||
@@ -350,8 +356,8 @@ export const draw = function (text, id, _version, diagObj) {
|
||||
g.setParent(diagObj.db.lookUpDomId(subG.nodes[j]), diagObj.db.lookUpDomId(subG.id));
|
||||
}
|
||||
}
|
||||
addVertices(vert, g, id, root, doc, diagObj);
|
||||
addEdges(edges, g, diagObj);
|
||||
await addVertices(vert, g, id, root, doc, diagObj);
|
||||
await addEdges(edges, g, diagObj);
|
||||
|
||||
// Create the renderer
|
||||
const render = new Render();
|
||||
|
||||
@@ -27,7 +27,7 @@ describe('the flowchart renderer', function () {
|
||||
['cylinder', 'cylinder'],
|
||||
['group', 'rect'],
|
||||
].forEach(function ([type, expectedShape, expectedRadios = 0]) {
|
||||
it(`should add the correct shaped node to the graph for vertex type ${type}`, function () {
|
||||
it(`should add the correct shaped node to the graph for vertex type ${type}`, async function () {
|
||||
const fakeDiag = {
|
||||
db: {
|
||||
lookUpDomId: () => {
|
||||
@@ -41,7 +41,7 @@ describe('the flowchart renderer', function () {
|
||||
addedNodes.push([id, object]);
|
||||
},
|
||||
};
|
||||
addVertices(
|
||||
await addVertices(
|
||||
{
|
||||
v1: {
|
||||
type,
|
||||
@@ -70,7 +70,7 @@ describe('the flowchart renderer', function () {
|
||||
['Multi<br>Line', 'Multi<br/>Line', 'Multi<br />Line', 'Multi<br\t/>Line'].forEach(function (
|
||||
labelText
|
||||
) {
|
||||
it('should handle multiline texts with different line breaks', function () {
|
||||
it('should handle multiline texts with different line breaks', async function () {
|
||||
const addedNodes = [];
|
||||
const fakeDiag = {
|
||||
db: {
|
||||
@@ -84,7 +84,7 @@ describe('the flowchart renderer', function () {
|
||||
addedNodes.push([id, object]);
|
||||
},
|
||||
};
|
||||
addVertices(
|
||||
await addVertices(
|
||||
{
|
||||
v1: {
|
||||
type: 'rect',
|
||||
@@ -121,7 +121,7 @@ describe('the flowchart renderer', function () {
|
||||
'color:#ccc;text-align:center;',
|
||||
],
|
||||
].forEach(function ([style, expectedStyle, expectedLabelStyle]) {
|
||||
it(`should add the styles to style and/or labelStyle for style ${style}`, function () {
|
||||
it(`should add the styles to style and/or labelStyle for style ${style}`, async function () {
|
||||
const addedNodes = [];
|
||||
const fakeDiag = {
|
||||
db: {
|
||||
@@ -135,7 +135,7 @@ describe('the flowchart renderer', function () {
|
||||
addedNodes.push([id, object]);
|
||||
},
|
||||
};
|
||||
addVertices(
|
||||
await addVertices(
|
||||
{
|
||||
v1: {
|
||||
type: 'rect',
|
||||
@@ -160,7 +160,7 @@ describe('the flowchart renderer', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it(`should add default class to all nodes which do not have another class assigned`, function () {
|
||||
it(`should add default class to all nodes which do not have another class assigned`, async function () {
|
||||
const addedNodes = [];
|
||||
const mockG = {
|
||||
setNode: function (id, object) {
|
||||
@@ -174,7 +174,7 @@ describe('the flowchart renderer', function () {
|
||||
},
|
||||
},
|
||||
};
|
||||
addVertices(
|
||||
await addVertices(
|
||||
{
|
||||
v1: {
|
||||
type: 'rect',
|
||||
@@ -206,7 +206,7 @@ describe('the flowchart renderer', function () {
|
||||
});
|
||||
|
||||
describe('when adding edges to a graph', function () {
|
||||
it('should handle multiline texts and set centered label position', function () {
|
||||
it('should handle multiline texts and set centered label position', async function () {
|
||||
const addedEdges = [];
|
||||
const fakeDiag = {
|
||||
db: {
|
||||
@@ -220,7 +220,7 @@ describe('the flowchart renderer', function () {
|
||||
addedEdges.push(data);
|
||||
},
|
||||
};
|
||||
addEdges(
|
||||
await addEdges(
|
||||
[
|
||||
{ text: 'Multi<br>Line' },
|
||||
{ text: 'Multi<br/>Line' },
|
||||
@@ -251,7 +251,7 @@ describe('the flowchart renderer', function () {
|
||||
'fill:red;',
|
||||
],
|
||||
].forEach(function ([style, expectedStyle, expectedLabelStyle]) {
|
||||
it(`should add the styles to style and/or labelStyle for style ${style}`, function () {
|
||||
it(`should add the styles to style and/or labelStyle for style ${style}`, async function () {
|
||||
const addedEdges = [];
|
||||
const fakeDiag = {
|
||||
db: {
|
||||
@@ -265,7 +265,7 @@ describe('the flowchart renderer', function () {
|
||||
addedEdges.push(data);
|
||||
},
|
||||
};
|
||||
addEdges([{ style: style, text: 'styling' }], mockG, fakeDiag);
|
||||
await addEdges([{ style: style, text: 'styling' }], mockG, fakeDiag);
|
||||
|
||||
expect(addedEdges).toHaveLength(1);
|
||||
expect(addedEdges[0]).toHaveProperty('style', expectedStyle);
|
||||
|
||||
@@ -1,503 +0,0 @@
|
||||
/** mermaid
|
||||
* https://mermaidjs.github.io/
|
||||
* (c) 2015 Knut Sveidqvist
|
||||
* MIT license.
|
||||
*/
|
||||
|
||||
/* lexical grammar */
|
||||
%lex
|
||||
%x string
|
||||
|
||||
%%
|
||||
\%\%[^\n]* /* do nothing */
|
||||
["] 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';
|
||||
"click" return 'CLICK';
|
||||
"graph" return 'GRAPH';
|
||||
"subgraph" return 'subgraph';
|
||||
"end"\b\s* return 'end';
|
||||
"LR" return 'DIR';
|
||||
"RL" return 'DIR';
|
||||
"TB" return 'DIR';
|
||||
"BT" return 'DIR';
|
||||
"TD" return 'DIR';
|
||||
"BR" return 'DIR';
|
||||
[0-9]+ return 'NUM';
|
||||
\# return 'BRKT';
|
||||
":" return 'COLON';
|
||||
";" return 'SEMI';
|
||||
"," return 'COMMA';
|
||||
"*" return 'MULT';
|
||||
\s*\-\-[x]\s* return 'ARROW_CROSS';
|
||||
\s*[x]\-\-[x]\s* return 'DOUBLE_ARROW_CROSS';
|
||||
\s*\-\-\>\s* return 'ARROW_POINT';
|
||||
\s*\<\-\-\>\s* return 'DOUBLE_ARROW_POINT';
|
||||
\s*\-\-[o]\s* return 'ARROW_CIRCLE';
|
||||
\s*[o]\-\-[o]\s* return 'DOUBLE_ARROW_CIRCLE';
|
||||
\s*\-\-\-\s* return 'ARROW_OPEN';
|
||||
\s*\-\.\-[x]\s* return 'DOTTED_ARROW_CROSS';
|
||||
\s*[x]\-\.\-[x]\s* return 'DOUBLE_DOTTED_ARROW_CROSS';
|
||||
\s*\-\.\-\>\s* return 'DOTTED_ARROW_POINT';
|
||||
\s*\<\-\.\-\>\s* return 'DOUBLE_DOTTED_ARROW_POINT';
|
||||
\s*\-\.\-[o]\s* return 'DOTTED_ARROW_CIRCLE';
|
||||
\s*[o]\-\.\-[o]\s* return 'DOUBLE_DOTTED_ARROW_CIRCLE';
|
||||
\s*\-\.\-\s* return 'DOTTED_ARROW_OPEN';
|
||||
\s*.\-[x]\s* return 'DOTTED_ARROW_CROSS';
|
||||
\s*[x].\-[x]\s* return 'DOUBLE_DOTTED_ARROW_CROSS';
|
||||
\s*\.\-\>\s* return 'DOTTED_ARROW_POINT';
|
||||
\s*\<\.\-\>\s* return 'DOUBLE_DOTTED_ARROW_POINT';
|
||||
\s*\.\-[o]\s* return 'DOTTED_ARROW_CIRCLE';
|
||||
\s*[o]\.\-[o]\s* return 'DOUBLE_DOTTED_ARROW_CIRCLE';
|
||||
\s*\.\-\s* return 'DOTTED_ARROW_OPEN';
|
||||
\s*\=\=[x]\s* return 'THICK_ARROW_CROSS';
|
||||
\s*[x]\=\=[x]\s* return 'DOUBLE_THICK_ARROW_CROSS';
|
||||
\s*\=\=\>\s* return 'THICK_ARROW_POINT';
|
||||
\s*\<\=\=\>\s* return 'DOUBLE_THICK_ARROW_POINT';
|
||||
\s*\=\=[o]\s* return 'THICK_ARROW_CIRCLE';
|
||||
\s*[o]\=\=[o]\s* return 'DOUBLE_THICK_ARROW_CIRCLE';
|
||||
\s*\=\=[\=]\s* return 'THICK_ARROW_OPEN';
|
||||
\s*\-\-\s* return '--';
|
||||
\s*\-\.\s* return '-.';
|
||||
\s*\=\=\s* return '==';
|
||||
%\s*\<\-\-\s* return 'START_DOUBLE_ARROW_POINT';
|
||||
%\s*\[x]\-\-\s* return 'START_DOUBLE_ARROW_CROSS';
|
||||
%\s*\[o]\-\-\s* return 'START_DOUBLE_ARROW_CIRCLE';
|
||||
%\s*\<\-\.\s* return 'START_DOUBLE_DOTTED_ARROW_POINT';
|
||||
%\s*\[x]\-\.\s* return 'START_DOUBLE_DOTTED_ARROW_CROSS';
|
||||
%\s*\[o]\-\.\s* return 'START_DOUBLE_DOTTED_ARROW_CIRCLE';
|
||||
%\s*\<\=\=\s* return 'START_DOUBLE_THICK_ARROW_POINT';
|
||||
%\s*\[x]\=\=\s* return 'START_DOUBLE_THICK_ARROW_CROSS';
|
||||
%\s*\[o]\=\=\s* return 'START_DOUBLE_THICK_ARROW_CIRCLE';
|
||||
"(-" return '(-';
|
||||
"-)" return '-)';
|
||||
\- return 'MINUS';
|
||||
"." return 'DOT';
|
||||
\+ return 'PLUS';
|
||||
\% return 'PCT';
|
||||
"=" return 'EQUALS';
|
||||
\= return 'EQUALS';
|
||||
"<" return 'TAGSTART';
|
||||
">" return 'TAGEND';
|
||||
"^" return 'UP';
|
||||
"v" return 'DOWN';
|
||||
[A-Za-z]+ return 'ALPHA';
|
||||
[!"#$%&'*+,-.`?\\_/] return 'PUNCTUATION';
|
||||
[\u00AA\u00B5\u00BA\u00C0-\u00D6\u00D8-\u00F6]|
|
||||
[\u00F8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377]|
|
||||
[\u037A-\u037D\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5]|
|
||||
[\u03F7-\u0481\u048A-\u0527\u0531-\u0556\u0559\u0561-\u0587\u05D0-\u05EA]|
|
||||
[\u05F0-\u05F2\u0620-\u064A\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE]|
|
||||
[\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA]|
|
||||
[\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u08A0]|
|
||||
[\u08A2-\u08AC\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971-\u0977]|
|
||||
[\u0979-\u097F\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2]|
|
||||
[\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u0A05-\u0A0A]|
|
||||
[\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39]|
|
||||
[\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8]|
|
||||
[\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0B05-\u0B0C]|
|
||||
[\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C]|
|
||||
[\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99]|
|
||||
[\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0]|
|
||||
[\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C33\u0C35-\u0C39\u0C3D]|
|
||||
[\u0C58\u0C59\u0C60\u0C61\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3]|
|
||||
[\u0CB5-\u0CB9\u0CBD\u0CDE\u0CE0\u0CE1\u0CF1\u0CF2\u0D05-\u0D0C\u0D0E-\u0D10]|
|
||||
[\u0D12-\u0D3A\u0D3D\u0D4E\u0D60\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1]|
|
||||
[\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E81]|
|
||||
[\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3]|
|
||||
[\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6]|
|
||||
[\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A]|
|
||||
[\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081]|
|
||||
[\u108E\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D]|
|
||||
[\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0]|
|
||||
[\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310]|
|
||||
[\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F4\u1401-\u166C]|
|
||||
[\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u1700-\u170C\u170E-\u1711]|
|
||||
[\u1720-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7]|
|
||||
[\u17DC\u1820-\u1877\u1880-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191C]|
|
||||
[\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19C1-\u19C7\u1A00-\u1A16]|
|
||||
[\u1A20-\u1A54\u1AA7\u1B05-\u1B33\u1B45-\u1B4B\u1B83-\u1BA0\u1BAE\u1BAF]|
|
||||
[\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1CE9-\u1CEC]|
|
||||
[\u1CEE-\u1CF1\u1CF5\u1CF6\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D]|
|
||||
[\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D]|
|
||||
[\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3]|
|
||||
[\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F]|
|
||||
[\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128]|
|
||||
[\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2183\u2184]|
|
||||
[\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3]|
|
||||
[\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6]|
|
||||
[\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE]|
|
||||
[\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005\u3006\u3031-\u3035\u303B\u303C]|
|
||||
[\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312D]|
|
||||
[\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FCC]|
|
||||
[\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A\uA62B]|
|
||||
[\uA640-\uA66E\uA67F-\uA697\uA6A0-\uA6E5\uA717-\uA71F\uA722-\uA788]|
|
||||
[\uA78B-\uA78E\uA790-\uA793\uA7A0-\uA7AA\uA7F8-\uA801\uA803-\uA805]|
|
||||
[\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB]|
|
||||
[\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF\uAA00-\uAA28]|
|
||||
[\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA76\uAA7A\uAA80-\uAAAF\uAAB1\uAAB5]|
|
||||
[\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4]|
|
||||
[\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E]|
|
||||
[\uABC0-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D]|
|
||||
[\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36]|
|
||||
[\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D]|
|
||||
[\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';
|
||||
"|" return 'PIPE';
|
||||
"(" return 'PS';
|
||||
")" return 'PE';
|
||||
"[" return 'SQS';
|
||||
"]" return 'SQE';
|
||||
"{" return 'DIAMOND_START'
|
||||
"}" return 'DIAMOND_STOP'
|
||||
"\"" return 'QUOTE';
|
||||
\n+ return 'NEWLINE';
|
||||
\s return 'SPACE';
|
||||
<<EOF>> return 'EOF';
|
||||
|
||||
/lex
|
||||
|
||||
/* operator associations and precedence */
|
||||
|
||||
%left '^'
|
||||
|
||||
%start mermaidDoc
|
||||
|
||||
%% /* language grammar */
|
||||
|
||||
mermaidDoc: graphConfig document;
|
||||
|
||||
document
|
||||
: /* empty */
|
||||
{ $$ = [];}
|
||||
| document line
|
||||
{
|
||||
if($2 !== []){
|
||||
$1.push($2);
|
||||
}
|
||||
$$=$1;}
|
||||
;
|
||||
|
||||
line
|
||||
: statement
|
||||
{$$=$1;}
|
||||
| SEMI
|
||||
| NEWLINE
|
||||
| SPACE
|
||||
| EOF
|
||||
;
|
||||
|
||||
graphConfig
|
||||
: SPACE graphConfig
|
||||
| NEWLINE graphConfig
|
||||
| GRAPH SPACE DIR FirstStmtSeperator
|
||||
{ yy.setDirection($3);$$ = $3;}
|
||||
| GRAPH SPACE TAGEND FirstStmtSeperator
|
||||
{ yy.setDirection("LR");$$ = $3;}
|
||||
| GRAPH SPACE TAGSTART FirstStmtSeperator
|
||||
{ yy.setDirection("RL");$$ = $3;}
|
||||
| GRAPH SPACE UP FirstStmtSeperator
|
||||
{ yy.setDirection("BT");$$ = $3;}
|
||||
| GRAPH SPACE DOWN FirstStmtSeperator
|
||||
{ yy.setDirection("TB");$$ = $3;}
|
||||
;
|
||||
|
||||
ending: endToken ending
|
||||
| endToken
|
||||
;
|
||||
|
||||
endToken: NEWLINE | SPACE | EOF;
|
||||
|
||||
FirstStmtSeperator
|
||||
: SEMI | NEWLINE | spaceList NEWLINE ;
|
||||
|
||||
|
||||
spaceListNewline
|
||||
: SPACE spaceListNewline
|
||||
| NEWLINE spaceListNewline
|
||||
| NEWLINE
|
||||
| SPACE
|
||||
;
|
||||
|
||||
|
||||
spaceList
|
||||
: SPACE spaceList
|
||||
| SPACE
|
||||
;
|
||||
|
||||
statement
|
||||
: verticeStatement separator
|
||||
{$$=$1}
|
||||
| styleStatement separator
|
||||
{$$=[];}
|
||||
| linkStyleStatement separator
|
||||
{$$=[];}
|
||||
| classDefStatement separator
|
||||
{$$=[];}
|
||||
| classStatement separator
|
||||
{$$=[];}
|
||||
| clickStatement separator
|
||||
{$$=[];}
|
||||
| subgraph SPACE alphaNum SQS text SQE separator document end
|
||||
{$$=yy.addSubGraph($3,$8,$5);}
|
||||
| subgraph SPACE STR separator document end
|
||||
{$$=yy.addSubGraph(undefined,$5,$3);}
|
||||
| subgraph SPACE alphaNum separator document end
|
||||
{$$=yy.addSubGraph($3,$5,$3);}
|
||||
| subgraph separator document end
|
||||
{$$=yy.addSubGraph(undefined,$3,undefined);}
|
||||
;
|
||||
|
||||
separator: NEWLINE | SEMI | EOF ;
|
||||
|
||||
verticeStatement:
|
||||
vertex link vertex
|
||||
{ yy.addLink($1,$3,$2);$$ = [$1,$3];}
|
||||
| vertex
|
||||
{$$ = [$1];}
|
||||
;
|
||||
|
||||
vertex: alphaNum SQS text SQE
|
||||
{$$ = $1;yy.addVertex($1,$3,'square');}
|
||||
| alphaNum SQS text SQE spaceList
|
||||
{$$ = $1;yy.addVertex($1,$3,'square');}
|
||||
| alphaNum PS PS text PE PE
|
||||
{$$ = $1;yy.addVertex($1,$4,'circle');}
|
||||
| alphaNum PS PS text PE PE spaceList
|
||||
{$$ = $1;yy.addVertex($1,$4,'circle');}
|
||||
| alphaNum '(-' text '-)'
|
||||
{$$ = $1;yy.addVertex($1,$3,'ellipse');}
|
||||
| alphaNum '(-' text '-)' spaceList
|
||||
{$$ = $1;yy.addVertex($1,$3,'ellipse');}
|
||||
| alphaNum PS text PE
|
||||
{$$ = $1;yy.addVertex($1,$3,'round');}
|
||||
| alphaNum PS text PE spaceList
|
||||
{$$ = $1;yy.addVertex($1,$3,'round');}
|
||||
| alphaNum DIAMOND_START text DIAMOND_STOP
|
||||
{$$ = $1;yy.addVertex($1,$3,'diamond');}
|
||||
| alphaNum DIAMOND_START text DIAMOND_STOP spaceList
|
||||
{$$ = $1;yy.addVertex($1,$3,'diamond');}
|
||||
| alphaNum TAGEND text SQE
|
||||
{$$ = $1;yy.addVertex($1,$3,'odd');}
|
||||
| alphaNum TAGEND text SQE spaceList
|
||||
{$$ = $1;yy.addVertex($1,$3,'odd');}
|
||||
/* | alphaNum SQS text TAGSTART
|
||||
{$$ = $1;yy.addVertex($1,$3,'odd_right');}
|
||||
| alphaNum SQS text TAGSTART spaceList
|
||||
{$$ = $1;yy.addVertex($1,$3,'odd_right');} */
|
||||
| alphaNum
|
||||
{$$ = $1;yy.addVertex($1);}
|
||||
| alphaNum spaceList
|
||||
{$$ = $1;yy.addVertex($1);}
|
||||
;
|
||||
|
||||
alphaNum
|
||||
: alphaNumStatement
|
||||
{$$=$1;}
|
||||
| alphaNum alphaNumStatement
|
||||
{$$=$1+''+$2;}
|
||||
;
|
||||
|
||||
alphaNumStatement
|
||||
: DIR
|
||||
{$$=$1;}
|
||||
| alphaNumToken
|
||||
{$$=$1;}
|
||||
| DOWN
|
||||
{$$='v';}
|
||||
| MINUS
|
||||
{$$='-';}
|
||||
;
|
||||
|
||||
link: linkStatement arrowText
|
||||
{$1.text = $2;$$ = $1;}
|
||||
| linkStatement TESTSTR SPACE
|
||||
{$1.text = $2;$$ = $1;}
|
||||
| linkStatement arrowText SPACE
|
||||
{$1.text = $2;$$ = $1;}
|
||||
| linkStatement
|
||||
{$$ = $1;}
|
||||
| '--' text ARROW_POINT
|
||||
{$$ = {"type":"arrow","stroke":"normal","text":$2};}
|
||||
| 'START_DOUBLE_ARROW_POINT' text ARROW_POINT
|
||||
{$$ = {"type":"double_arrow_point","stroke":"normal","text":$2};}
|
||||
| '--' text ARROW_CIRCLE
|
||||
{$$ = {"type":"arrow_circle","stroke":"normal","text":$2};}
|
||||
| '--' text ARROW_CROSS
|
||||
{$$ = {"type":"arrow_cross","stroke":"normal","text":$2};}
|
||||
| '--' text ARROW_OPEN
|
||||
{$$ = {"type":"arrow_open","stroke":"normal","text":$2};}
|
||||
| '-.' text DOTTED_ARROW_POINT
|
||||
{$$ = {"type":"arrow","stroke":"dotted","text":$2};}
|
||||
| '-.' text DOTTED_ARROW_CIRCLE
|
||||
{$$ = {"type":"arrow_circle","stroke":"dotted","text":$2};}
|
||||
| '-.' text DOTTED_ARROW_CROSS
|
||||
{$$ = {"type":"arrow_cross","stroke":"dotted","text":$2};}
|
||||
| '-.' text DOTTED_ARROW_OPEN
|
||||
{$$ = {"type":"arrow_open","stroke":"dotted","text":$2};}
|
||||
| '==' text THICK_ARROW_POINT
|
||||
{$$ = {"type":"arrow","stroke":"thick","text":$2};}
|
||||
| '==' text THICK_ARROW_CIRCLE
|
||||
{$$ = {"type":"arrow_circle","stroke":"thick","text":$2};}
|
||||
| '==' text THICK_ARROW_CROSS
|
||||
{$$ = {"type":"arrow_cross","stroke":"thick","text":$2};}
|
||||
| '==' text THICK_ARROW_OPEN
|
||||
{$$ = {"type":"arrow_open","stroke":"thick","text":$2};}
|
||||
;
|
||||
|
||||
linkStatement: ARROW_POINT
|
||||
{$$ = {"type":"arrow","stroke":"normal"};}
|
||||
| DOUBLE_ARROW_POINT
|
||||
{$$ = {"type":"double_arrow_point","stroke":"normal"};}
|
||||
| ARROW_CIRCLE
|
||||
{$$ = {"type":"arrow_circle","stroke":"normal"};}
|
||||
% | DOUBLE_ARROW_CIRCLE
|
||||
% {$$ = {"type":"double_arrow_circle","stroke":"normal"};}
|
||||
| ARROW_CROSS
|
||||
{$$ = {"type":"arrow_cross","stroke":"normal"};}
|
||||
% | DOUBLE_ARROW_CROSS
|
||||
% {$$ = {"type":"double_arrow_cross","stroke":"normal"};}
|
||||
| ARROW_OPEN
|
||||
{$$ = {"type":"arrow_open","stroke":"normal"};}
|
||||
| DOTTED_ARROW_POINT
|
||||
{$$ = {"type":"arrow","stroke":"dotted"};}
|
||||
% | DOUEBL_DOTTED_ARROW_POINT
|
||||
% {$$ = {"type":"doueble_arrow_point","stroke":"dotted"};}
|
||||
| DOTTED_ARROW_CIRCLE
|
||||
{$$ = {"type":"arrow_circle","stroke":"dotted"};}
|
||||
% | DOTTED_ARROW_CIRCLE
|
||||
% {$$ = {"type":"double_arrow_circle","stroke":"dotted"};}
|
||||
| DOTTED_ARROW_CROSS
|
||||
{$$ = {"type":"arrow_cross","stroke":"dotted"};}
|
||||
% | DOTTED_ARROW_CROSS
|
||||
% {$$ = {"type":"double_arrow_cross","stroke":"dotted"};}
|
||||
| DOTTED_ARROW_OPEN
|
||||
{$$ = {"type":"arrow_open","stroke":"dotted"};}
|
||||
| THICK_ARROW_POINT
|
||||
{$$ = {"type":"arrow","stroke":"thick"};}
|
||||
% | THICK_ARROW_POINT
|
||||
% {$$ = {"type":"double_arrow_point","stroke":"thick"};}
|
||||
| THICK_ARROW_CIRCLE
|
||||
{$$ = {"type":"arrow_circle","stroke":"thick"};}
|
||||
% | THICK_ARROW_CIRCLE
|
||||
% {$$ = {"type":"double_arrow_circle","stroke":"thick"};}
|
||||
| THICK_ARROW_CROSS
|
||||
{$$ = {"type":"arrow_cross","stroke":"thick"};}
|
||||
% | THICK_ARROW_CROSS
|
||||
% {$$ = {"type":"double_arrow_cross","stroke":"thick"};}
|
||||
| THICK_ARROW_OPEN
|
||||
{$$ = {"type":"arrow_open","stroke":"thick"};}
|
||||
;
|
||||
|
||||
arrowText:
|
||||
PIPE text PIPE
|
||||
{$$ = $2;}
|
||||
;
|
||||
|
||||
text: textToken
|
||||
{$$=$1;}
|
||||
| text textToken
|
||||
{$$=$1+''+$2;}
|
||||
| STR
|
||||
{$$=$1;}
|
||||
;
|
||||
|
||||
|
||||
|
||||
commentText: commentToken
|
||||
{$$=$1;}
|
||||
| commentText commentToken
|
||||
{$$=$1+''+$2;}
|
||||
;
|
||||
|
||||
|
||||
keywords
|
||||
: STYLE | LINKSTYLE | CLASSDEF | CLASS | CLICK | GRAPH | DIR | subgraph | end | DOWN | UP;
|
||||
|
||||
|
||||
textNoTags: textNoTagsToken
|
||||
{$$=$1;}
|
||||
| textNoTags textNoTagsToken
|
||||
{$$=$1+''+$2;}
|
||||
;
|
||||
|
||||
|
||||
classDefStatement:CLASSDEF SPACE DEFAULT SPACE stylesOpt
|
||||
{$$ = $1;yy.addClass($3,$5);}
|
||||
| CLASSDEF SPACE alphaNum SPACE stylesOpt
|
||||
{$$ = $1;yy.addClass($3,$5);}
|
||||
;
|
||||
|
||||
classStatement:CLASS SPACE alphaNum SPACE alphaNum
|
||||
{$$ = $1;yy.setClass($3, $5);}
|
||||
;
|
||||
|
||||
clickStatement
|
||||
: CLICK SPACE alphaNum SPACE alphaNum {$$ = $1;yy.setClickEvent($3, $5, undefined);}
|
||||
| CLICK SPACE alphaNum SPACE alphaNum SPACE STR {$$ = $1;yy.setClickEvent($3, $5, $7) ;}
|
||||
| CLICK SPACE alphaNum SPACE STR {$$ = $1;yy.setLink($3, $5, undefined);}
|
||||
| CLICK SPACE alphaNum SPACE STR SPACE STR {$$ = $1;yy.setLink($3, $5, $7 );}
|
||||
;
|
||||
|
||||
styleStatement:STYLE SPACE alphaNum SPACE stylesOpt
|
||||
{$$ = $1;yy.addVertex($3,undefined,undefined,$5);}
|
||||
| STYLE SPACE HEX SPACE stylesOpt
|
||||
{$$ = $1;yy.updateLink($3,$5);}
|
||||
;
|
||||
|
||||
linkStyleStatement
|
||||
: LINKSTYLE SPACE DEFAULT SPACE stylesOpt
|
||||
{$$ = $1;yy.updateLink([$3],$5);}
|
||||
| LINKSTYLE SPACE numList SPACE stylesOpt
|
||||
{$$ = $1;yy.updateLink($3,$5);}
|
||||
| LINKSTYLE SPACE DEFAULT SPACE INTERPOLATE SPACE alphaNum SPACE stylesOpt
|
||||
{$$ = $1;yy.updateLinkInterpolate([$3],$7);yy.updateLink([$3],$9);}
|
||||
| LINKSTYLE SPACE numList SPACE INTERPOLATE SPACE alphaNum SPACE stylesOpt
|
||||
{$$ = $1;yy.updateLinkInterpolate($3,$7);yy.updateLink($3,$9);}
|
||||
| LINKSTYLE SPACE DEFAULT SPACE INTERPOLATE SPACE alphaNum
|
||||
{$$ = $1;yy.updateLinkInterpolate([$3],$7);}
|
||||
| LINKSTYLE SPACE numList SPACE INTERPOLATE SPACE alphaNum
|
||||
{$$ = $1;yy.updateLinkInterpolate($3,$7);}
|
||||
;
|
||||
|
||||
commentStatement: PCT PCT commentText;
|
||||
|
||||
numList: NUM
|
||||
{$$ = [$1]}
|
||||
| numList COMMA NUM
|
||||
{$1.push($3);$$ = $1;}
|
||||
;
|
||||
|
||||
stylesOpt: style
|
||||
{$$ = [$1]}
|
||||
| stylesOpt COMMA style
|
||||
{$1.push($3);$$ = $1;}
|
||||
;
|
||||
|
||||
style: styleComponent
|
||||
|style styleComponent
|
||||
{$$ = $1 + $2;}
|
||||
;
|
||||
|
||||
styleComponent: ALPHA | COLON | MINUS | NUM | UNIT | SPACE | HEX | BRKT | DOT | STYLE | PCT ;
|
||||
|
||||
/* Token lists */
|
||||
|
||||
commentToken : textToken | graphCodeTokens ;
|
||||
|
||||
textToken : textNoTagsToken | TAGSTART | TAGEND | '==' | '--' | PCT | DEFAULT;
|
||||
|
||||
textNoTagsToken: alphaNumToken | SPACE | MINUS | keywords ;
|
||||
|
||||
alphaNumToken : ALPHA | PUNCTUATION | UNICODE_TEXT | NUM | COLON | COMMA | PLUS | EQUALS | MULT | DOT | BRKT ;
|
||||
|
||||
graphCodeTokens: PIPE | PS | PE | SQS | SQE | DIAMOND_START | DIAMOND_STOP | TAG_START | TAG_END | ARROW_CROSS | ARROW_POINT | ARROW_CIRCLE | ARROW_OPEN | QUOTE | SEMI ;
|
||||
%%
|
||||
@@ -292,15 +292,15 @@ graphConfig
|
||||
| NEWLINE graphConfig
|
||||
| GRAPH NODIR
|
||||
{ yy.setDirection('TB');$$ = 'TB';}
|
||||
| GRAPH DIR FirstStmtSeperator
|
||||
| GRAPH DIR FirstStmtSeparator
|
||||
{ yy.setDirection($DIR);$$ = $DIR;}
|
||||
// | GRAPH SPACE TAGEND FirstStmtSeperator
|
||||
// | GRAPH SPACE TAGEND FirstStmtSeparator
|
||||
// { yy.setDirection("LR");$$ = $TAGEND;}
|
||||
// | GRAPH SPACE TAGSTART FirstStmtSeperator
|
||||
// | GRAPH SPACE TAGSTART FirstStmtSeparator
|
||||
// { yy.setDirection("RL");$$ = $TAGSTART;}
|
||||
// | GRAPH SPACE UP FirstStmtSeperator
|
||||
// | GRAPH SPACE UP FirstStmtSeparator
|
||||
// { yy.setDirection("BT");$$ = $UP;}
|
||||
// | GRAPH SPACE DOWN FirstStmtSeperator
|
||||
// | GRAPH SPACE DOWN FirstStmtSeparator
|
||||
// { yy.setDirection("TB");$$ = $DOWN;}
|
||||
;
|
||||
|
||||
@@ -310,7 +310,7 @@ ending: endToken ending
|
||||
|
||||
endToken: NEWLINE | SPACE | EOF;
|
||||
|
||||
FirstStmtSeperator
|
||||
FirstStmtSeparator
|
||||
: SEMI | NEWLINE | spaceList NEWLINE ;
|
||||
|
||||
|
||||
@@ -328,8 +328,8 @@ spaceList
|
||||
;
|
||||
|
||||
statement
|
||||
: verticeStatement separator
|
||||
{ /* console.warn('finat vs', $verticeStatement.nodes); */ $$=$verticeStatement.nodes}
|
||||
: vertexStatement separator
|
||||
{ /* console.warn('finat vs', $vertexStatement.nodes); */ $$=$vertexStatement.nodes}
|
||||
| styleStatement separator
|
||||
{$$=[];}
|
||||
| linkStyleStatement separator
|
||||
@@ -357,10 +357,10 @@ statement
|
||||
separator: NEWLINE | SEMI | EOF ;
|
||||
|
||||
|
||||
verticeStatement: verticeStatement link node
|
||||
{ /* 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',$verticeStatement.stmt,$node); */ yy.addLink($verticeStatement.stmt,$node,$link); $$ = { stmt: $node, nodes: $node.concat($verticeStatement.nodes) } }
|
||||
vertexStatement: vertexStatement link node
|
||||
{ /* console.warn('vs',$vertexStatement.stmt,$node); */ yy.addLink($vertexStatement.stmt,$node,$link); $$ = { stmt: $node, nodes: $node.concat($vertexStatement.nodes) } }
|
||||
| vertexStatement link node spaceList
|
||||
{ /* console.warn('vs',$vertexStatement.stmt,$node); */ yy.addLink($vertexStatement.stmt,$node,$link); $$ = { stmt: $node, nodes: $node.concat($vertexStatement.nodes) } }
|
||||
|node spaceList {/*console.warn('noda', $node);*/ $$ = {stmt: $node, nodes:$node }}
|
||||
|node { /*console.warn('noda', $node);*/ $$ = {stmt: $node, nodes:$node }}
|
||||
;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import flowDb from '../flowDb.js';
|
||||
import flow from './flow.jison';
|
||||
import { setConfig } from '../../../config.js';
|
||||
import { cleanupComments } from '../../../diagram-api/comments.js';
|
||||
import { setConfig } from '../../../config.js';
|
||||
|
||||
setConfig({
|
||||
securityLevel: 'strict',
|
||||
|
||||
@@ -66,6 +66,12 @@ const getStyles = (options: FlowChartStyleOptions) =>
|
||||
// text-anchor: start;
|
||||
// }
|
||||
|
||||
.node .katex path {
|
||||
fill: #000;
|
||||
stroke: #000;
|
||||
stroke-width: 1px;
|
||||
}
|
||||
|
||||
.node .label {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
53
packages/mermaid/src/diagrams/flowchart/types.ts
Normal file
53
packages/mermaid/src/diagrams/flowchart/types.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
export interface FlowVertex {
|
||||
classes: string[];
|
||||
dir?: string;
|
||||
domId: string;
|
||||
haveCallback?: boolean;
|
||||
id: string;
|
||||
labelType: 'text';
|
||||
link?: string;
|
||||
linkTarget?: string;
|
||||
props?: any;
|
||||
styles: string[];
|
||||
text?: string;
|
||||
type?: string;
|
||||
}
|
||||
|
||||
export interface FlowText {
|
||||
text: string;
|
||||
type: 'text';
|
||||
}
|
||||
|
||||
export interface FlowEdge {
|
||||
start: string;
|
||||
end: string;
|
||||
interpolate?: string;
|
||||
type?: string;
|
||||
stroke?: string;
|
||||
style?: string[];
|
||||
length?: number;
|
||||
text: string;
|
||||
labelType: 'text';
|
||||
}
|
||||
|
||||
export interface FlowClass {
|
||||
id: string;
|
||||
styles: string[];
|
||||
textStyles: string[];
|
||||
}
|
||||
|
||||
export interface FlowSubGraph {
|
||||
classes: string[];
|
||||
dir?: string;
|
||||
id: string;
|
||||
labelType: string;
|
||||
nodes: string[];
|
||||
title: string;
|
||||
}
|
||||
|
||||
export interface FlowLink {
|
||||
length?: number;
|
||||
stroke: string;
|
||||
type: string;
|
||||
text?: string;
|
||||
}
|
||||
@@ -21,6 +21,7 @@ dayjs.extend(dayjsIsoWeek);
|
||||
dayjs.extend(dayjsCustomParseFormat);
|
||||
dayjs.extend(dayjsAdvancedFormat);
|
||||
|
||||
const WEEKEND_START_DAY = { friday: 5, saturday: 6 };
|
||||
let dateFormat = '';
|
||||
let axisFormat = '';
|
||||
let tickInterval = undefined;
|
||||
@@ -37,6 +38,7 @@ let funs = [];
|
||||
let inclusiveEndDates = false;
|
||||
let topAxis = false;
|
||||
let weekday = 'sunday';
|
||||
let weekend = 'saturday';
|
||||
|
||||
// The serial order of the task in the script
|
||||
let lastOrder = 0;
|
||||
@@ -63,6 +65,7 @@ export const clear = function () {
|
||||
links = {};
|
||||
commonClear();
|
||||
weekday = 'sunday';
|
||||
weekend = 'saturday';
|
||||
};
|
||||
|
||||
export const setAxisFormat = function (txt) {
|
||||
@@ -167,7 +170,11 @@ export const isInvalidDate = function (date, dateFormat, excludes, includes) {
|
||||
if (includes.includes(date.format(dateFormat.trim()))) {
|
||||
return false;
|
||||
}
|
||||
if (date.isoWeekday() >= 6 && excludes.includes('weekends')) {
|
||||
if (
|
||||
excludes.includes('weekends') &&
|
||||
(date.isoWeekday() === WEEKEND_START_DAY[weekend] ||
|
||||
date.isoWeekday() === WEEKEND_START_DAY[weekend] + 1)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (excludes.includes(date.format('dddd').toLowerCase())) {
|
||||
@@ -184,6 +191,10 @@ export const getWeekday = function () {
|
||||
return weekday;
|
||||
};
|
||||
|
||||
export const setWeekend = function (startDay) {
|
||||
weekend = startDay;
|
||||
};
|
||||
|
||||
/**
|
||||
* TODO: fully document what this function does and what types it accepts
|
||||
*
|
||||
@@ -256,32 +267,25 @@ const getStartDate = function (prevTime, dateFormat, str) {
|
||||
str = str.trim();
|
||||
|
||||
// Test for after
|
||||
const re = /^after\s+([\d\w- ]+)/;
|
||||
const afterStatement = re.exec(str.trim());
|
||||
const afterRePattern = /^after\s+(?<ids>[\d\w- ]+)/;
|
||||
const afterStatement = afterRePattern.exec(str);
|
||||
|
||||
if (afterStatement !== null) {
|
||||
// check all after ids and take the latest
|
||||
let latestEndingTask = null;
|
||||
afterStatement[1].split(' ').forEach(function (id) {
|
||||
let latestTask = null;
|
||||
for (const id of afterStatement.groups.ids.split(' ')) {
|
||||
let task = findTaskById(id);
|
||||
if (task !== undefined) {
|
||||
if (!latestEndingTask) {
|
||||
latestEndingTask = task;
|
||||
} else {
|
||||
if (task.endTime > latestEndingTask.endTime) {
|
||||
latestEndingTask = task;
|
||||
}
|
||||
}
|
||||
if (task !== undefined && (!latestTask || task.endTime > latestTask.endTime)) {
|
||||
latestTask = task;
|
||||
}
|
||||
});
|
||||
|
||||
if (!latestEndingTask) {
|
||||
const dt = new Date();
|
||||
dt.setHours(0, 0, 0, 0);
|
||||
return dt;
|
||||
} else {
|
||||
return latestEndingTask.endTime;
|
||||
}
|
||||
|
||||
if (latestTask) {
|
||||
return latestTask.endTime;
|
||||
}
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
return today;
|
||||
}
|
||||
|
||||
// Check for actual date set
|
||||
@@ -332,6 +336,7 @@ const getStartDate = function (prevTime, dateFormat, str) {
|
||||
* @returns {[value: number, unit: dayjs.ManipulateType]} Arguments to pass to `dayjs.add()`
|
||||
*/
|
||||
const parseDuration = function (str) {
|
||||
// cspell:disable-next-line
|
||||
const statement = /^(\d+(?:\.\d+)?)([Mdhmswy]|ms)$/.exec(str.trim());
|
||||
if (statement !== null) {
|
||||
return [Number.parseFloat(statement[1]), statement[2]];
|
||||
@@ -343,13 +348,35 @@ const parseDuration = function (str) {
|
||||
const getEndDate = function (prevTime, dateFormat, str, inclusive = false) {
|
||||
str = str.trim();
|
||||
|
||||
// Check for actual date
|
||||
let mDate = dayjs(str, dateFormat.trim(), true);
|
||||
if (mDate.isValid()) {
|
||||
if (inclusive) {
|
||||
mDate = mDate.add(1, 'd');
|
||||
// test for until
|
||||
const untilRePattern = /^until\s+(?<ids>[\d\w- ]+)/;
|
||||
const untilStatement = untilRePattern.exec(str);
|
||||
|
||||
if (untilStatement !== null) {
|
||||
// check all until ids and take the earliest
|
||||
let earliestTask = null;
|
||||
for (const id of untilStatement.groups.ids.split(' ')) {
|
||||
let task = findTaskById(id);
|
||||
if (task !== undefined && (!earliestTask || task.startTime < earliestTask.startTime)) {
|
||||
earliestTask = task;
|
||||
}
|
||||
}
|
||||
return mDate.toDate();
|
||||
|
||||
if (earliestTask) {
|
||||
return earliestTask.startTime;
|
||||
}
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
return today;
|
||||
}
|
||||
|
||||
// check for actual date
|
||||
let parsedDate = dayjs(str, dateFormat.trim(), true);
|
||||
if (parsedDate.isValid()) {
|
||||
if (inclusive) {
|
||||
parsedDate = parsedDate.add(1, 'd');
|
||||
}
|
||||
return parsedDate.toDate();
|
||||
}
|
||||
|
||||
let endTime = dayjs(prevTime);
|
||||
@@ -765,6 +792,7 @@ export default {
|
||||
isInvalidDate,
|
||||
setWeekday,
|
||||
getWeekday,
|
||||
setWeekend,
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -140,10 +140,10 @@ describe('when using the ganttDb', function () {
|
||||
|
||||
it('should handle relative start date based on id regardless of sections', function () {
|
||||
ganttDb.setDateFormat('YYYY-MM-DD');
|
||||
ganttDb.addSection('testa1');
|
||||
ganttDb.addSection('sec1');
|
||||
ganttDb.addTask('test1', 'id1,2013-01-01,2w');
|
||||
ganttDb.addTask('test2', 'id2,after id3,1d');
|
||||
ganttDb.addSection('testa2');
|
||||
ganttDb.addSection('sec2');
|
||||
ganttDb.addTask('test3', 'id3,after id1,2d');
|
||||
|
||||
const tasks = ganttDb.getTasks();
|
||||
@@ -158,6 +158,58 @@ describe('when using the ganttDb', function () {
|
||||
expect(tasks[2].startTime).toEqual(new Date(2013, 0, 15));
|
||||
expect(tasks[2].endTime).toEqual(new Date(2013, 0, 17));
|
||||
});
|
||||
|
||||
it('should handle relative end date based on id regardless of sections', function () {
|
||||
ganttDb.setDateFormat('YYYY-MM-DD');
|
||||
ganttDb.addSection('sec1');
|
||||
ganttDb.addTask('task1', 'id1,2013-01-01,until id3');
|
||||
ganttDb.addSection('sec2');
|
||||
ganttDb.addTask('task2', 'id2,2013-01-10,until id3');
|
||||
ganttDb.addTask('task3', 'id3,2013-02-01,2d');
|
||||
|
||||
const tasks = ganttDb.getTasks();
|
||||
|
||||
expect(tasks[0].startTime).toEqual(new Date(2013, 0, 1));
|
||||
expect(tasks[0].endTime).toEqual(new Date(2013, 1, 1));
|
||||
expect(tasks[0].id).toEqual('id1');
|
||||
expect(tasks[0].task).toEqual('task1');
|
||||
|
||||
expect(tasks[1].id).toEqual('id2');
|
||||
expect(tasks[1].task).toEqual('task2');
|
||||
expect(tasks[1].startTime).toEqual(new Date(2013, 0, 10));
|
||||
expect(tasks[1].endTime).toEqual(new Date(2013, 1, 1));
|
||||
});
|
||||
|
||||
it('should handle relative start date based on multiple id', function () {
|
||||
ganttDb.setDateFormat('YYYY-MM-DD');
|
||||
ganttDb.addSection('sec1');
|
||||
ganttDb.addTask('task1', 'id1,after id2 id3 id4,1d');
|
||||
ganttDb.addTask('task2', 'id2,2013-01-01,1d');
|
||||
ganttDb.addTask('task3', 'id3,2013-02-01,3d');
|
||||
ganttDb.addTask('task4', 'id4,2013-02-01,2d');
|
||||
|
||||
const tasks = ganttDb.getTasks();
|
||||
|
||||
expect(tasks[0].endTime).toEqual(new Date(2013, 1, 5));
|
||||
expect(tasks[0].id).toEqual('id1');
|
||||
expect(tasks[0].task).toEqual('task1');
|
||||
});
|
||||
|
||||
it('should handle relative end date based on multiple id', function () {
|
||||
ganttDb.setDateFormat('YYYY-MM-DD');
|
||||
ganttDb.addSection('sec1');
|
||||
ganttDb.addTask('task1', 'id1,2013-01-01,until id2 id3 id4');
|
||||
ganttDb.addTask('task2', 'id2,2013-01-11,1d');
|
||||
ganttDb.addTask('task3', 'id3,2013-02-10,1d');
|
||||
ganttDb.addTask('task4', 'id4,2013-02-12,1d');
|
||||
|
||||
const tasks = ganttDb.getTasks();
|
||||
|
||||
expect(tasks[0].endTime).toEqual(new Date(2013, 0, 11));
|
||||
expect(tasks[0].id).toEqual('id1');
|
||||
expect(tasks[0].task).toEqual('task1');
|
||||
});
|
||||
|
||||
it('should ignore weekends', function () {
|
||||
ganttDb.setDateFormat('YYYY-MM-DD');
|
||||
ganttDb.setExcludes('weekends 2019-02-06,friday');
|
||||
@@ -215,6 +267,21 @@ describe('when using the ganttDb', function () {
|
||||
expect(tasks[6].task).toEqual('test7');
|
||||
});
|
||||
|
||||
it('should ignore weekends starting on friday', function () {
|
||||
ganttDb.setDateFormat('YYYY-MM-DD');
|
||||
ganttDb.setExcludes('weekends');
|
||||
ganttDb.setWeekend('friday');
|
||||
ganttDb.addSection('friday-saturday weekends skip test');
|
||||
ganttDb.addTask('test1', 'id1,2024-02-28, 3d');
|
||||
|
||||
const tasks = ganttDb.getTasks();
|
||||
|
||||
expect(tasks[0].startTime).toEqual(dayjs('2024-02-28', 'YYYY-MM-DD').toDate());
|
||||
expect(tasks[0].endTime).toEqual(dayjs('2024-03-04', 'YYYY-MM-DD').toDate());
|
||||
expect(tasks[0].id).toEqual('id1');
|
||||
expect(tasks[0].task).toEqual('test1');
|
||||
});
|
||||
|
||||
it('should maintain the order in which tasks are created', function () {
|
||||
ganttDb.setAccTitle('Project Execution');
|
||||
ganttDb.setDateFormat('YYYY-MM-DD');
|
||||
|
||||
@@ -178,7 +178,7 @@ export const draw = function (text, id, version, diagObj) {
|
||||
// tasks are created based on their order of startTime
|
||||
taskArray.sort(taskCompare);
|
||||
|
||||
makeGant(taskArray, w, h);
|
||||
makeGantt(taskArray, w, h);
|
||||
|
||||
configureSvgSize(svg, h, w, conf.useMaxWidth);
|
||||
|
||||
@@ -194,7 +194,7 @@ export const draw = function (text, id, version, diagObj) {
|
||||
* @param pageWidth
|
||||
* @param pageHeight
|
||||
*/
|
||||
function makeGant(tasks, pageWidth, pageHeight) {
|
||||
function makeGantt(tasks, pageWidth, pageHeight) {
|
||||
const barHeight = conf.barHeight;
|
||||
const gap = barHeight + conf.barGap;
|
||||
const topPadding = conf.topPadding;
|
||||
@@ -695,12 +695,12 @@ export const draw = function (text, id, version, diagObj) {
|
||||
function vertLabels(theGap, theTopPad) {
|
||||
let prevGap = 0;
|
||||
|
||||
const numOccurances = Object.keys(categoryHeights).map((d) => [d, categoryHeights[d]]);
|
||||
const numOccurrences = Object.keys(categoryHeights).map((d) => [d, categoryHeights[d]]);
|
||||
|
||||
svg
|
||||
.append('g') // without doing this, impossible to put grid lines behind text
|
||||
.selectAll('text')
|
||||
.data(numOccurances)
|
||||
.data(numOccurrences)
|
||||
.enter()
|
||||
.append(function (d) {
|
||||
const rows = d[0].split(common.lineBreakRegex);
|
||||
@@ -725,7 +725,7 @@ export const draw = function (text, id, version, diagObj) {
|
||||
.attr('y', function (d, i) {
|
||||
if (i > 0) {
|
||||
for (let j = 0; j < i; j++) {
|
||||
prevGap += numOccurances[i - 1][1];
|
||||
prevGap += numOccurrences[i - 1][1];
|
||||
return (d[1] * theGap) / 2 + prevGap * theGap + theTopPad;
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -84,6 +84,8 @@ weekday\s+thursday return 'weekday_thursday'
|
||||
weekday\s+friday return 'weekday_friday'
|
||||
weekday\s+saturday return 'weekday_saturday'
|
||||
weekday\s+sunday return 'weekday_sunday'
|
||||
weekend\s+friday return 'weekend_friday'
|
||||
weekend\s+saturday return 'weekend_saturday'
|
||||
\d\d\d\d"-"\d\d"-"\d\d return 'date';
|
||||
"title"\s[^\n]+ return 'title';
|
||||
"accDescription"\s[^#\n;]+ return 'accDescription'
|
||||
@@ -128,6 +130,11 @@ weekday
|
||||
| weekday_sunday { yy.setWeekday("sunday");}
|
||||
;
|
||||
|
||||
weekend
|
||||
: weekend_friday { yy.setWeekend("friday");}
|
||||
| weekend_saturday { yy.setWeekend("saturday");}
|
||||
;
|
||||
|
||||
statement
|
||||
: dateFormat {yy.setDateFormat($1.substr(11));$$=$1.substr(11);}
|
||||
| inclusiveEndDates {yy.enableInclusiveEndDates();$$=$1.substr(18);}
|
||||
@@ -138,6 +145,7 @@ statement
|
||||
| includes {yy.setIncludes($1.substr(9));$$=$1.substr(9);}
|
||||
| todayMarker {yy.setTodayMarker($1.substr(12));$$=$1.substr(12);}
|
||||
| weekday
|
||||
| weekend
|
||||
| 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($$); }
|
||||
|
||||
@@ -118,6 +118,38 @@ describe('when parsing a gantt diagram it', function () {
|
||||
expect(tasks[0].id).toEqual('des1');
|
||||
expect(tasks[0].task).toEqual('Design jison grammar');
|
||||
});
|
||||
it('should handle a task with start/end time relative to other tasks', function () {
|
||||
const str =
|
||||
'gantt\n' +
|
||||
'dateFormat YYYY-MM-DD\n' +
|
||||
'title Adding gantt diagram functionality to mermaid\n' +
|
||||
'section Documentation\n' +
|
||||
'task A: a, 2024-01-27, 2024-01-28\n' +
|
||||
'task B: b, after a, 2024-01-30\n' +
|
||||
'task C: c, 2024-01-20, until a\n' +
|
||||
'task D: d, after c, until b';
|
||||
|
||||
expect(parserFnConstructor(str)).not.toThrow();
|
||||
|
||||
const tasks = parser.yy.getTasks();
|
||||
|
||||
expect(tasks[0].startTime).toEqual(new Date(2024, 0, 27));
|
||||
expect(tasks[0].endTime).toEqual(new Date(2024, 0, 28));
|
||||
expect(tasks[0].id).toEqual('a');
|
||||
expect(tasks[0].task).toEqual('task A');
|
||||
expect(tasks[1].startTime).toEqual(new Date(2024, 0, 28));
|
||||
expect(tasks[1].endTime).toEqual(new Date(2024, 0, 30));
|
||||
expect(tasks[1].id).toEqual('b');
|
||||
expect(tasks[1].task).toEqual('task B');
|
||||
expect(tasks[2].startTime).toEqual(new Date(2024, 0, 20));
|
||||
expect(tasks[2].endTime).toEqual(new Date(2024, 0, 27));
|
||||
expect(tasks[2].id).toEqual('c');
|
||||
expect(tasks[2].task).toEqual('task C');
|
||||
expect(tasks[3].startTime).toEqual(new Date(2024, 0, 27));
|
||||
expect(tasks[3].endTime).toEqual(new Date(2024, 0, 28));
|
||||
expect(tasks[3].id).toEqual('d');
|
||||
expect(tasks[3].task).toEqual('task D');
|
||||
});
|
||||
it.each(convert`
|
||||
tags | milestone | done | crit | active
|
||||
${'milestone'} | ${true} | ${false} | ${false} | ${false}
|
||||
|
||||
@@ -36,8 +36,8 @@ function getId() {
|
||||
// * @param otherCommit
|
||||
// */
|
||||
// eslint-disable-next-line @cspell/spellchecker
|
||||
// function isfastforwardable(currentCommit, otherCommit) {
|
||||
// log.debug('Entering isfastforwardable:', currentCommit.id, otherCommit.id);
|
||||
// function isFastForwardable(currentCommit, otherCommit) {
|
||||
// log.debug('Entering isFastForwardable:', currentCommit.id, otherCommit.id);
|
||||
// let cnt = 0;
|
||||
// while (currentCommit.seq <= otherCommit.seq && currentCommit !== otherCommit && cnt < 1000) {
|
||||
// cnt++;
|
||||
@@ -46,8 +46,8 @@ function getId() {
|
||||
// if (Array.isArray(otherCommit.parent)) {
|
||||
// log.debug('In merge commit:', otherCommit.parent);
|
||||
// return (
|
||||
// isfastforwardable(currentCommit, commits[otherCommit.parent[0]]) ||
|
||||
// isfastforwardable(currentCommit, commits[otherCommit.parent[1]])
|
||||
// isFastForwardable(currentCommit, commits[otherCommit.parent[0]]) ||
|
||||
// isFastForwardable(currentCommit, commits[otherCommit.parent[1]])
|
||||
// );
|
||||
// } else {
|
||||
// otherCommit = commits[otherCommit.parent];
|
||||
@@ -64,7 +64,7 @@ function getId() {
|
||||
// function isReachableFrom(currentCommit, otherCommit) {
|
||||
// const currentSeq = currentCommit.seq;
|
||||
// const otherSeq = otherCommit.seq;
|
||||
// if (currentSeq > otherSeq) return isfastforwardable(otherCommit, currentCommit);
|
||||
// if (currentSeq > otherSeq) return isFastForwardable(otherCommit, currentCommit);
|
||||
// return false;
|
||||
// }
|
||||
|
||||
@@ -231,7 +231,7 @@ export const merge = function (otherBranch, custom_id, override_type, custom_tag
|
||||
// log.debug('Already merged');
|
||||
// return;
|
||||
// }
|
||||
// if (isfastforwardable(currentCommit, otherCommit)) {
|
||||
// if (isFastForwardable(currentCommit, otherCommit)) {
|
||||
// branches[curBranch] = branches[otherBranch];
|
||||
// head = commits[branches[curBranch]];
|
||||
// } else {
|
||||
|
||||
@@ -295,7 +295,7 @@ function renderCommitHistory(svg, commitId, branches, direction) {
|
||||
}
|
||||
|
||||
if (Array.isArray(commitId)) {
|
||||
logger.debug('found merge commmit', commitId);
|
||||
logger.debug('found merge commit', commitId);
|
||||
renderCommitHistory(svg, commitId[0], branches, direction);
|
||||
branchNum++;
|
||||
renderCommitHistory(svg, commitId[1], branches, direction);
|
||||
|
||||
@@ -456,6 +456,10 @@ const drawArrow = (svg, commitA, commitB, allCommits) => {
|
||||
let radius = 0;
|
||||
let offset = 0;
|
||||
let colorClassNum = branchPos[commitB.branch].index;
|
||||
if (commitB.type === commitType.MERGE && commitA.id !== commitB.parents[0]) {
|
||||
colorClassNum = branchPos[commitA.branch].index;
|
||||
}
|
||||
|
||||
let lineDef;
|
||||
if (arrowNeedsRerouting) {
|
||||
arc = 'A 10 10, 0, 0, 0,';
|
||||
@@ -470,7 +474,6 @@ const drawArrow = (svg, commitA, commitB, allCommits) => {
|
||||
if (p1.x < p2.x) {
|
||||
// Source commit is on branch position left of destination commit
|
||||
// so render arrow rightward with colour of destination branch
|
||||
colorClassNum = branchPos[commitB.branch].index;
|
||||
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}`;
|
||||
@@ -486,7 +489,6 @@ const drawArrow = (svg, commitA, commitB, allCommits) => {
|
||||
if (p1.y < p2.y) {
|
||||
// Source commit is on branch positioned above destination commit
|
||||
// so render arrow downward with colour of destination branch
|
||||
colorClassNum = branchPos[commitB.branch].index;
|
||||
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}`;
|
||||
@@ -500,19 +502,22 @@ const drawArrow = (svg, commitA, commitB, allCommits) => {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
arc = 'A 20 20, 0, 0, 0,';
|
||||
arc2 = 'A 20 20, 0, 0, 1,';
|
||||
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[commitB.branch].index;
|
||||
|
||||
lineDef = `M ${p1.x} ${p1.y} L ${p2.x - radius} ${p1.y} ${arc2} ${p2.x} ${
|
||||
p1.y + offset
|
||||
} L ${p2.x} ${p2.y}`;
|
||||
if (commitB.type === commitType.MERGE && commitA.id !== commitB.parents[0]) {
|
||||
lineDef = `M ${p1.x} ${p1.y} L ${p1.x} ${p2.y - radius} ${arc} ${p1.x + offset} ${
|
||||
p2.y
|
||||
} L ${p2.x} ${p2.y}`;
|
||||
} else {
|
||||
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,';
|
||||
@@ -520,46 +525,46 @@ const drawArrow = (svg, commitA, commitB, allCommits) => {
|
||||
radius = 20;
|
||||
offset = 20;
|
||||
|
||||
// Arrows going up take the color from the source branch
|
||||
colorClassNum = branchPos[commitA.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 (commitB.type === commitType.MERGE && commitA.id !== commitB.parents[0]) {
|
||||
lineDef = `M ${p1.x} ${p1.y} L ${p1.x} ${p2.y - radius} ${arc2} ${p1.x - offset} ${
|
||||
p2.y
|
||||
} L ${p2.x} ${p2.y}`;
|
||||
} else {
|
||||
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.x === p2.x) {
|
||||
colorClassNum = branchPos[commitA.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}`;
|
||||
lineDef = `M ${p1.x} ${p1.y} L ${p2.x} ${p2.y}`;
|
||||
}
|
||||
} else {
|
||||
if (p1.y < p2.y) {
|
||||
arc = 'A 20 20, 0, 0, 0,';
|
||||
radius = 20;
|
||||
offset = 20;
|
||||
// Arrows going up take the color from the target branch
|
||||
colorClassNum = branchPos[commitB.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 (commitB.type === commitType.MERGE && commitA.id !== commitB.parents[0]) {
|
||||
lineDef = `M ${p1.x} ${p1.y} L ${p2.x - radius} ${p1.y} ${arc2} ${p2.x} ${
|
||||
p1.y + offset
|
||||
} L ${p2.x} ${p2.y}`;
|
||||
} else {
|
||||
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[commitA.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 (commitB.type === commitType.MERGE && commitA.id !== commitB.parents[0]) {
|
||||
lineDef = `M ${p1.x} ${p1.y} L ${p2.x - radius} ${p1.y} ${arc} ${p2.x} ${
|
||||
p1.y - offset
|
||||
} L ${p2.x} ${p2.y}`;
|
||||
} else {
|
||||
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[commitA.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}`;
|
||||
lineDef = `M ${p1.x} ${p1.y} L ${p2.x} ${p2.y}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ options
|
||||
| NL
|
||||
;
|
||||
body
|
||||
: /*emmpty*/ {$$ = []}
|
||||
: /*empty*/ {$$ = []}
|
||||
| body line {$1.push($2); $$=$1;}
|
||||
;
|
||||
line
|
||||
|
||||
@@ -1,24 +1,27 @@
|
||||
// @ts-ignore - jison doesn't export types
|
||||
import { parser } from './parser/info.jison';
|
||||
import { db } from './infoDb.js';
|
||||
import { parser } from './infoParser.js';
|
||||
|
||||
describe('info diagram', () => {
|
||||
beforeEach(() => {
|
||||
parser.yy = db;
|
||||
parser.yy.clear();
|
||||
});
|
||||
|
||||
it('should handle an info definition', () => {
|
||||
describe('info', () => {
|
||||
it('should handle an info definition', async () => {
|
||||
const str = `info`;
|
||||
parser.parse(str);
|
||||
|
||||
expect(db.getInfo()).toBeFalsy();
|
||||
await expect(parser.parse(str)).resolves.not.toThrow();
|
||||
});
|
||||
|
||||
it('should handle an info definition with showInfo', () => {
|
||||
it('should handle an info definition with showInfo', async () => {
|
||||
const str = `info showInfo`;
|
||||
parser.parse(str);
|
||||
await expect(parser.parse(str)).resolves.not.toThrow();
|
||||
});
|
||||
|
||||
expect(db.getInfo()).toBeTruthy();
|
||||
it('should throw because of unsupported info grammar', async () => {
|
||||
const str = `info unsupported`;
|
||||
await expect(parser.parse(str)).rejects.toThrow(
|
||||
'Parsing failed: unexpected character: ->u<- at offset: 5, skipped 11 characters.'
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw because of unsupported info grammar', async () => {
|
||||
const str = `info unsupported`;
|
||||
await expect(parser.parse(str)).rejects.toThrow(
|
||||
'Parsing failed: unexpected character: ->u<- at offset: 5, skipped 11 characters.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,23 +1,10 @@
|
||||
import type { InfoFields, InfoDB } from './infoTypes.js';
|
||||
import { version } from '../../../package.json';
|
||||
|
||||
export const DEFAULT_INFO_DB: InfoFields = {
|
||||
info: false,
|
||||
} as const;
|
||||
export const DEFAULT_INFO_DB: InfoFields = { version } as const;
|
||||
|
||||
let info: boolean = DEFAULT_INFO_DB.info;
|
||||
|
||||
export const setInfo = (toggle: boolean): void => {
|
||||
info = toggle;
|
||||
};
|
||||
|
||||
export const getInfo = (): boolean => info;
|
||||
|
||||
const clear = (): void => {
|
||||
info = DEFAULT_INFO_DB.info;
|
||||
};
|
||||
export const getVersion = (): string => DEFAULT_INFO_DB.version;
|
||||
|
||||
export const db: InfoDB = {
|
||||
clear,
|
||||
setInfo,
|
||||
getInfo,
|
||||
getVersion,
|
||||
};
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { DiagramDefinition } from '../../diagram-api/types.js';
|
||||
// @ts-ignore - jison doesn't export types
|
||||
import parser from './parser/info.jison';
|
||||
import { parser } from './infoParser.js';
|
||||
import { db } from './infoDb.js';
|
||||
import { renderer } from './infoRenderer.js';
|
||||
|
||||
|
||||
11
packages/mermaid/src/diagrams/info/infoParser.ts
Normal file
11
packages/mermaid/src/diagrams/info/infoParser.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import type { Info } from '@mermaid-js/parser';
|
||||
import { parse } from '@mermaid-js/parser';
|
||||
import type { ParserDefinition } from '../../diagram-api/types.js';
|
||||
import { log } from '../../logger.js';
|
||||
|
||||
export const parser: ParserDefinition = {
|
||||
parse: async (input: string): Promise<void> => {
|
||||
const ast: Info = await parse('info', input);
|
||||
log.debug(ast);
|
||||
},
|
||||
};
|
||||
@@ -1,11 +1,9 @@
|
||||
import type { DiagramDB } from '../../diagram-api/types.js';
|
||||
|
||||
export interface InfoFields {
|
||||
info: boolean;
|
||||
version: string;
|
||||
}
|
||||
|
||||
export interface InfoDB extends DiagramDB {
|
||||
clear: () => void;
|
||||
setInfo: (info: boolean) => void;
|
||||
getInfo: () => boolean;
|
||||
getVersion: () => string;
|
||||
}
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
/** mermaid
|
||||
* https://knsv.github.io/mermaid
|
||||
* (c) 2015 Knut Sveidqvist
|
||||
* MIT license.
|
||||
*/
|
||||
%lex
|
||||
|
||||
%options case-insensitive
|
||||
|
||||
%{
|
||||
// Pre-lexer code can go here
|
||||
%}
|
||||
|
||||
%%
|
||||
|
||||
"info" return 'info' ;
|
||||
[\s\n\r]+ return 'NL' ;
|
||||
[\s]+ return 'space';
|
||||
"showInfo" return 'showInfo';
|
||||
<<EOF>> return 'EOF' ;
|
||||
. return 'TXT' ;
|
||||
|
||||
/lex
|
||||
|
||||
%start start
|
||||
|
||||
%% /* language grammar */
|
||||
|
||||
start
|
||||
// %{ : info document 'EOF' { return yy; } }
|
||||
: info document 'EOF' { return yy; }
|
||||
;
|
||||
|
||||
document
|
||||
: /* empty */
|
||||
| document line
|
||||
;
|
||||
|
||||
line
|
||||
: statement { }
|
||||
| 'NL'
|
||||
;
|
||||
|
||||
statement
|
||||
: showInfo { yy.setInfo(true); }
|
||||
;
|
||||
|
||||
%%
|
||||
@@ -1,12 +1,13 @@
|
||||
// @ts-ignore: JISON doesn't support types
|
||||
import mindmapParser from './parser/mindmap.jison';
|
||||
import * as mindmapDb from './mindmapDb.js';
|
||||
import mindmapRenderer from './mindmapRenderer.js';
|
||||
import mindmapStyles from './styles.js';
|
||||
import parser from './parser/mindmap.jison';
|
||||
import db from './mindmapDb.js';
|
||||
import renderer from './mindmapRenderer.js';
|
||||
import styles from './styles.js';
|
||||
import type { DiagramDefinition } from '../../diagram-api/types.js';
|
||||
|
||||
export const diagram = {
|
||||
db: mindmapDb,
|
||||
renderer: mindmapRenderer,
|
||||
parser: mindmapParser,
|
||||
styles: mindmapStyles,
|
||||
export const diagram: DiagramDefinition = {
|
||||
db,
|
||||
renderer,
|
||||
parser,
|
||||
styles,
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// @ts-expect-error No types available for JISON
|
||||
import { parser as mindmap } from './parser/mindmap.jison';
|
||||
import * as mindmapDB from './mindmapDb.js';
|
||||
import mindmapDB from './mindmapDb.js';
|
||||
// Todo fix utils functions for tests
|
||||
import { setLogLevel } from '../../diagram-api/diagramAPI.js';
|
||||
|
||||
@@ -11,7 +12,7 @@ describe('when parsing a mindmap ', function () {
|
||||
});
|
||||
describe('hiearchy', function () {
|
||||
it('MMP-1 should handle a simple root definition abc122', function () {
|
||||
let str = `mindmap
|
||||
const str = `mindmap
|
||||
root`;
|
||||
|
||||
mindmap.parse(str);
|
||||
@@ -19,7 +20,7 @@ describe('when parsing a mindmap ', function () {
|
||||
expect(mindmap.yy.getMindmap().descr).toEqual('root');
|
||||
});
|
||||
it('MMP-2 should handle a hierachial mindmap definition', function () {
|
||||
let str = `mindmap
|
||||
const str = `mindmap
|
||||
root
|
||||
child1
|
||||
child2
|
||||
@@ -34,7 +35,7 @@ describe('when parsing a mindmap ', function () {
|
||||
});
|
||||
|
||||
it('3 should handle a simple root definition with a shape and without an id abc123', function () {
|
||||
let str = `mindmap
|
||||
const str = `mindmap
|
||||
(root)`;
|
||||
|
||||
mindmap.parse(str);
|
||||
@@ -43,7 +44,7 @@ describe('when parsing a mindmap ', function () {
|
||||
});
|
||||
|
||||
it('MMP-4 should handle a deeper hierachial mindmap definition', function () {
|
||||
let str = `mindmap
|
||||
const str = `mindmap
|
||||
root
|
||||
child1
|
||||
leaf1
|
||||
@@ -58,40 +59,27 @@ describe('when parsing a mindmap ', function () {
|
||||
expect(mm.children[1].descr).toEqual('child2');
|
||||
});
|
||||
it('5 Multiple roots are illegal', function () {
|
||||
let str = `mindmap
|
||||
const str = `mindmap
|
||||
root
|
||||
fakeRoot`;
|
||||
|
||||
try {
|
||||
mindmap.parse(str);
|
||||
// Fail test if above expression doesn't throw anything.
|
||||
expect(true).toBe(false);
|
||||
} catch (e) {
|
||||
expect(e.message).toBe(
|
||||
'There can be only one root. No parent could be found for ("fakeRoot")'
|
||||
);
|
||||
}
|
||||
expect(() => mindmap.parse(str)).toThrow(
|
||||
'There can be only one root. No parent could be found for ("fakeRoot")'
|
||||
);
|
||||
});
|
||||
it('MMP-6 real root in wrong place', function () {
|
||||
let str = `mindmap
|
||||
const str = `mindmap
|
||||
root
|
||||
fakeRoot
|
||||
realRootWrongPlace`;
|
||||
|
||||
try {
|
||||
mindmap.parse(str);
|
||||
// Fail test if above expression doesn't throw anything.
|
||||
expect(true).toBe(false);
|
||||
} catch (e) {
|
||||
expect(e.message).toBe(
|
||||
'There can be only one root. No parent could be found for ("fakeRoot")'
|
||||
);
|
||||
}
|
||||
expect(() => mindmap.parse(str)).toThrow(
|
||||
'There can be only one root. No parent could be found for ("fakeRoot")'
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('nodes', function () {
|
||||
it('MMP-7 should handle an id and type for a node definition', function () {
|
||||
let str = `mindmap
|
||||
const str = `mindmap
|
||||
root[The root]
|
||||
`;
|
||||
|
||||
@@ -102,7 +90,7 @@ describe('when parsing a mindmap ', function () {
|
||||
expect(mm.type).toEqual(mindmap.yy.nodeType.RECT);
|
||||
});
|
||||
it('MMP-8 should handle an id and type for a node definition', function () {
|
||||
let str = `mindmap
|
||||
const str = `mindmap
|
||||
root
|
||||
theId(child1)`;
|
||||
|
||||
@@ -116,7 +104,7 @@ describe('when parsing a mindmap ', function () {
|
||||
expect(child.type).toEqual(mindmap.yy.nodeType.ROUNDED_RECT);
|
||||
});
|
||||
it('MMP-9 should handle an id and type for a node definition', function () {
|
||||
let str = `mindmap
|
||||
const str = `mindmap
|
||||
root
|
||||
theId(child1)`;
|
||||
|
||||
@@ -130,7 +118,7 @@ root
|
||||
expect(child.type).toEqual(mindmap.yy.nodeType.ROUNDED_RECT);
|
||||
});
|
||||
it('MMP-10 multiple types (circle)', function () {
|
||||
let str = `mindmap
|
||||
const str = `mindmap
|
||||
root((the root))
|
||||
`;
|
||||
|
||||
@@ -142,7 +130,7 @@ root
|
||||
});
|
||||
|
||||
it('MMP-11 multiple types (cloud)', function () {
|
||||
let str = `mindmap
|
||||
const str = `mindmap
|
||||
root)the root(
|
||||
`;
|
||||
|
||||
@@ -153,7 +141,7 @@ root
|
||||
expect(mm.type).toEqual(mindmap.yy.nodeType.CLOUD);
|
||||
});
|
||||
it('MMP-12 multiple types (bang)', function () {
|
||||
let str = `mindmap
|
||||
const str = `mindmap
|
||||
root))the root((
|
||||
`;
|
||||
|
||||
@@ -165,7 +153,7 @@ root
|
||||
});
|
||||
|
||||
it('MMP-12-a multiple types (hexagon)', function () {
|
||||
let str = `mindmap
|
||||
const str = `mindmap
|
||||
root{{the root}}
|
||||
`;
|
||||
|
||||
@@ -178,7 +166,7 @@ root
|
||||
});
|
||||
describe('decorations', function () {
|
||||
it('MMP-13 should be possible to set an icon for the node', function () {
|
||||
let str = `mindmap
|
||||
const str = `mindmap
|
||||
root[The root]
|
||||
::icon(bomb)
|
||||
`;
|
||||
@@ -192,7 +180,7 @@ root
|
||||
expect(mm.icon).toEqual('bomb');
|
||||
});
|
||||
it('MMP-14 should be possible to set classes for the node', function () {
|
||||
let str = `mindmap
|
||||
const str = `mindmap
|
||||
root[The root]
|
||||
:::m-4 p-8
|
||||
`;
|
||||
@@ -206,7 +194,7 @@ root
|
||||
expect(mm.class).toEqual('m-4 p-8');
|
||||
});
|
||||
it('MMP-15 should be possible to set both classes and icon for the node', function () {
|
||||
let str = `mindmap
|
||||
const str = `mindmap
|
||||
root[The root]
|
||||
:::m-4 p-8
|
||||
::icon(bomb)
|
||||
@@ -222,7 +210,7 @@ root
|
||||
expect(mm.icon).toEqual('bomb');
|
||||
});
|
||||
it('MMP-16 should be possible to set both classes and icon for the node', function () {
|
||||
let str = `mindmap
|
||||
const str = `mindmap
|
||||
root[The root]
|
||||
::icon(bomb)
|
||||
:::m-4 p-8
|
||||
@@ -240,7 +228,7 @@ root
|
||||
});
|
||||
describe('descriptions', function () {
|
||||
it('MMP-17 should be possible to use node syntax in the descriptions', function () {
|
||||
let str = `mindmap
|
||||
const str = `mindmap
|
||||
root["String containing []"]
|
||||
`;
|
||||
mindmap.parse(str);
|
||||
@@ -249,7 +237,7 @@ root
|
||||
expect(mm.descr).toEqual('String containing []');
|
||||
});
|
||||
it('MMP-18 should be possible to use node syntax in the descriptions in children', function () {
|
||||
let str = `mindmap
|
||||
const str = `mindmap
|
||||
root["String containing []"]
|
||||
child1["String containing ()"]
|
||||
`;
|
||||
@@ -261,7 +249,7 @@ root
|
||||
expect(mm.children[0].descr).toEqual('String containing ()');
|
||||
});
|
||||
it('MMP-19 should be possible to have a child after a class assignment', function () {
|
||||
let str = `mindmap
|
||||
const str = `mindmap
|
||||
root(Root)
|
||||
Child(Child)
|
||||
:::hot
|
||||
@@ -281,7 +269,7 @@ root
|
||||
});
|
||||
});
|
||||
it('MMP-20 should be possible to have meaningless empty rows in a mindmap abc124', function () {
|
||||
let str = `mindmap
|
||||
const str = `mindmap
|
||||
root(Root)
|
||||
Child(Child)
|
||||
a(a)
|
||||
@@ -300,7 +288,7 @@ root
|
||||
expect(child.children[1].nodeId).toEqual('b');
|
||||
});
|
||||
it('MMP-21 should be possible to have comments in a mindmap', function () {
|
||||
let str = `mindmap
|
||||
const str = `mindmap
|
||||
root(Root)
|
||||
Child(Child)
|
||||
a(a)
|
||||
@@ -321,7 +309,7 @@ root
|
||||
});
|
||||
|
||||
it('MMP-22 should be possible to have comments at the end of a line', function () {
|
||||
let str = `mindmap
|
||||
const str = `mindmap
|
||||
root(Root)
|
||||
Child(Child)
|
||||
a(a) %% This is a comment
|
||||
@@ -339,7 +327,7 @@ root
|
||||
expect(child.children[1].nodeId).toEqual('b');
|
||||
});
|
||||
it('MMP-23 Rows with only spaces should not interfere', function () {
|
||||
let str = 'mindmap\nroot\n A\n \n\n B';
|
||||
const str = 'mindmap\nroot\n A\n \n\n B';
|
||||
mindmap.parse(str);
|
||||
const mm = mindmap.yy.getMindmap();
|
||||
expect(mm.nodeId).toEqual('root');
|
||||
@@ -351,7 +339,7 @@ root
|
||||
expect(child2.nodeId).toEqual('B');
|
||||
});
|
||||
it('MMP-24 Handle rows above the mindmap declarations', function () {
|
||||
let str = '\n \nmindmap\nroot\n A\n \n\n B';
|
||||
const str = '\n \nmindmap\nroot\n A\n \n\n B';
|
||||
mindmap.parse(str);
|
||||
const mm = mindmap.yy.getMindmap();
|
||||
expect(mm.nodeId).toEqual('root');
|
||||
@@ -363,7 +351,7 @@ root
|
||||
expect(child2.nodeId).toEqual('B');
|
||||
});
|
||||
it('MMP-25 Handle rows above the mindmap declarations, no space', function () {
|
||||
let str = '\n\n\nmindmap\nroot\n A\n \n\n B';
|
||||
const str = '\n\n\nmindmap\nroot\n A\n \n\n B';
|
||||
mindmap.parse(str);
|
||||
const mm = mindmap.yy.getMindmap();
|
||||
expect(mm.nodeId).toEqual('root');
|
||||
@@ -1,19 +1,21 @@
|
||||
import { getConfig } from '../../diagram-api/diagramAPI.js';
|
||||
import { sanitizeText as _sanitizeText } from '../../diagrams/common/common.js';
|
||||
import type { D3Element } from '../../mermaidAPI.js';
|
||||
import { sanitizeText } from '../../diagrams/common/common.js';
|
||||
import { log } from '../../logger.js';
|
||||
import type { MindmapNode } from './mindmapTypes.js';
|
||||
import defaultConfig from '../../defaultConfig.js';
|
||||
|
||||
export const sanitizeText = (text) => _sanitizeText(text, getConfig());
|
||||
|
||||
let nodes = [];
|
||||
let nodes: MindmapNode[] = [];
|
||||
let cnt = 0;
|
||||
let elements = {};
|
||||
export const clear = () => {
|
||||
let elements: Record<number, D3Element> = {};
|
||||
|
||||
const clear = () => {
|
||||
nodes = [];
|
||||
cnt = 0;
|
||||
elements = {};
|
||||
};
|
||||
|
||||
const getParent = function (level) {
|
||||
const getParent = function (level: number) {
|
||||
for (let i = nodes.length - 1; i >= 0; i--) {
|
||||
if (nodes[i].level < level) {
|
||||
return nodes[i];
|
||||
@@ -23,34 +25,32 @@ const getParent = function (level) {
|
||||
return null;
|
||||
};
|
||||
|
||||
export const getMindmap = () => {
|
||||
const getMindmap = () => {
|
||||
return nodes.length > 0 ? nodes[0] : null;
|
||||
};
|
||||
export const addNode = (level, id, descr, type) => {
|
||||
|
||||
const addNode = (level: number, id: string, descr: string, type: number) => {
|
||||
log.info('addNode', level, id, descr, type);
|
||||
const conf = getConfig();
|
||||
let padding: number = conf.mindmap?.padding ?? defaultConfig.mindmap.padding;
|
||||
switch (type) {
|
||||
case nodeType.ROUNDED_RECT:
|
||||
case nodeType.RECT:
|
||||
case nodeType.HEXAGON:
|
||||
padding *= 2;
|
||||
}
|
||||
|
||||
const node = {
|
||||
id: cnt++,
|
||||
nodeId: sanitizeText(id),
|
||||
nodeId: sanitizeText(id, conf),
|
||||
level,
|
||||
descr: sanitizeText(descr),
|
||||
descr: sanitizeText(descr, conf),
|
||||
type,
|
||||
children: [],
|
||||
width: getConfig().mindmap.maxNodeWidth,
|
||||
};
|
||||
switch (node.type) {
|
||||
case nodeType.ROUNDED_RECT:
|
||||
node.padding = 2 * conf.mindmap.padding;
|
||||
break;
|
||||
case nodeType.RECT:
|
||||
node.padding = 2 * conf.mindmap.padding;
|
||||
break;
|
||||
case nodeType.HEXAGON:
|
||||
node.padding = 2 * conf.mindmap.padding;
|
||||
break;
|
||||
default:
|
||||
node.padding = conf.mindmap.padding;
|
||||
}
|
||||
width: conf.mindmap?.maxNodeWidth ?? defaultConfig.mindmap.maxNodeWidth,
|
||||
padding,
|
||||
} satisfies MindmapNode;
|
||||
|
||||
const parent = getParent(level);
|
||||
if (parent) {
|
||||
parent.children.push(node);
|
||||
@@ -62,22 +62,14 @@ export const addNode = (level, id, descr, type) => {
|
||||
nodes.push(node);
|
||||
} else {
|
||||
// Syntax error ... there can only bee one root
|
||||
let error = new Error(
|
||||
throw new Error(
|
||||
'There can be only one root. No parent could be found for ("' + node.descr + '")'
|
||||
);
|
||||
error.hash = {
|
||||
text: 'branch ' + name,
|
||||
token: 'branch ' + name,
|
||||
line: '1',
|
||||
loc: { first_line: 1, last_line: 1, first_column: 1, last_column: 1 },
|
||||
expected: ['"checkout ' + name + '"'],
|
||||
};
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const nodeType = {
|
||||
const nodeType = {
|
||||
DEFAULT: 0,
|
||||
NO_BORDER: 0,
|
||||
ROUNDED_RECT: 1,
|
||||
@@ -88,7 +80,7 @@ export const nodeType = {
|
||||
HEXAGON: 6,
|
||||
};
|
||||
|
||||
export const getType = (startStr, endStr) => {
|
||||
const getType = (startStr: string, endStr: string): number => {
|
||||
log.debug('In get type', startStr, endStr);
|
||||
switch (startStr) {
|
||||
case '[':
|
||||
@@ -108,21 +100,25 @@ export const getType = (startStr, endStr) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const setElementForId = (id, element) => {
|
||||
const setElementForId = (id: number, element: D3Element) => {
|
||||
elements[id] = element;
|
||||
};
|
||||
|
||||
export const decorateNode = (decoration) => {
|
||||
const node = nodes[nodes.length - 1];
|
||||
if (decoration && decoration.icon) {
|
||||
node.icon = sanitizeText(decoration.icon);
|
||||
const decorateNode = (decoration?: { class?: string; icon?: string }) => {
|
||||
if (!decoration) {
|
||||
return;
|
||||
}
|
||||
if (decoration && decoration.class) {
|
||||
node.class = sanitizeText(decoration.class);
|
||||
const config = getConfig();
|
||||
const node = nodes[nodes.length - 1];
|
||||
if (decoration.icon) {
|
||||
node.icon = sanitizeText(decoration.icon, config);
|
||||
}
|
||||
if (decoration.class) {
|
||||
node.class = sanitizeText(decoration.class, config);
|
||||
}
|
||||
};
|
||||
|
||||
export const type2Str = (type) => {
|
||||
const type2Str = (type: number) => {
|
||||
switch (type) {
|
||||
case nodeType.DEFAULT:
|
||||
return 'no-border';
|
||||
@@ -137,19 +133,27 @@ export const type2Str = (type) => {
|
||||
case nodeType.BANG:
|
||||
return 'bang';
|
||||
case nodeType.HEXAGON:
|
||||
return 'hexgon';
|
||||
return 'hexgon'; // cspell: disable-line
|
||||
default:
|
||||
return 'no-border';
|
||||
}
|
||||
};
|
||||
|
||||
export let parseError;
|
||||
export const setErrorHandler = (handler) => {
|
||||
parseError = handler;
|
||||
};
|
||||
|
||||
// Expose logger to grammar
|
||||
export const getLogger = () => log;
|
||||
const getLogger = () => log;
|
||||
const getElementById = (id: number) => elements[id];
|
||||
|
||||
export const getNodeById = (id) => nodes[id];
|
||||
export const getElementById = (id) => elements[id];
|
||||
const db = {
|
||||
clear,
|
||||
addNode,
|
||||
getMindmap,
|
||||
nodeType,
|
||||
getType,
|
||||
setElementForId,
|
||||
decorateNode,
|
||||
type2Str,
|
||||
getLogger,
|
||||
getElementById,
|
||||
} as const;
|
||||
|
||||
export default db;
|
||||
@@ -1,36 +1,53 @@
|
||||
/** Created by knut on 14-12-11. */
|
||||
import { select } from 'd3';
|
||||
import { log } from '../../logger.js';
|
||||
import { getConfig } from '../../diagram-api/diagramAPI.js';
|
||||
import { setupGraphViewbox } from '../../setupGraphViewbox.js';
|
||||
import svgDraw from './svgDraw.js';
|
||||
import cytoscape from 'cytoscape/dist/cytoscape.umd.js';
|
||||
import cytoscape from 'cytoscape';
|
||||
// @ts-expect-error No types available
|
||||
import coseBilkent from 'cytoscape-cose-bilkent';
|
||||
import * as db from './mindmapDb.js';
|
||||
import { select } from 'd3';
|
||||
import type { MermaidConfig } from '../../config.type.js';
|
||||
import { getConfig } from '../../diagram-api/diagramAPI.js';
|
||||
import type { DrawDefinition } from '../../diagram-api/types.js';
|
||||
import { log } from '../../logger.js';
|
||||
import type { D3Element } from '../../mermaidAPI.js';
|
||||
import { selectSvgElement } from '../../rendering-util/selectSvgElement.js';
|
||||
import { setupGraphViewbox } from '../../setupGraphViewbox.js';
|
||||
import type { FilledMindMapNode, MindmapDB, MindmapNode } from './mindmapTypes.js';
|
||||
import { drawNode, positionNode } from './svgDraw.js';
|
||||
import defaultConfig from '../../defaultConfig.js';
|
||||
|
||||
// Inject the layout algorithm into cytoscape
|
||||
cytoscape.use(coseBilkent);
|
||||
|
||||
/**
|
||||
* @param {any} svg The svg element to draw the diagram onto
|
||||
* @param {object} mindmap The mindmap data and hierarchy
|
||||
* @param section
|
||||
* @param {object} conf The configuration object
|
||||
*/
|
||||
function drawNodes(svg, mindmap, section, conf) {
|
||||
svgDraw.drawNode(svg, mindmap, section, conf);
|
||||
function drawNodes(
|
||||
db: MindmapDB,
|
||||
svg: D3Element,
|
||||
mindmap: FilledMindMapNode,
|
||||
section: number,
|
||||
conf: MermaidConfig
|
||||
) {
|
||||
drawNode(db, svg, mindmap, section, conf);
|
||||
if (mindmap.children) {
|
||||
mindmap.children.forEach((child, index) => {
|
||||
drawNodes(svg, child, section < 0 ? index : section, conf);
|
||||
drawNodes(db, svg, child, section < 0 ? index : section, conf);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param edgesEl
|
||||
* @param cy
|
||||
*/
|
||||
function drawEdges(edgesEl, cy) {
|
||||
declare module 'cytoscape' {
|
||||
interface EdgeSingular {
|
||||
_private: {
|
||||
bodyBounds: unknown;
|
||||
rscratch: {
|
||||
startX: number;
|
||||
startY: number;
|
||||
midX: number;
|
||||
midY: number;
|
||||
endX: number;
|
||||
endY: number;
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function drawEdges(edgesEl: D3Element, cy: cytoscape.Core) {
|
||||
cy.edges().map((edge, id) => {
|
||||
const data = edge.data();
|
||||
if (edge[0]._private.bodyBounds) {
|
||||
@@ -47,17 +64,11 @@ function drawEdges(edgesEl, cy) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mindmap The mindmap data and hierarchy
|
||||
* @param cy
|
||||
* @param conf The configuration object
|
||||
* @param level
|
||||
*/
|
||||
function addNodes(mindmap, cy, conf, level) {
|
||||
function addNodes(mindmap: MindmapNode, cy: cytoscape.Core, conf: MermaidConfig, level: number) {
|
||||
cy.add({
|
||||
group: 'nodes',
|
||||
data: {
|
||||
id: mindmap.id,
|
||||
id: mindmap.id.toString(),
|
||||
labelText: mindmap.descr,
|
||||
height: mindmap.height,
|
||||
width: mindmap.width,
|
||||
@@ -67,8 +78,8 @@ function addNodes(mindmap, cy, conf, level) {
|
||||
type: mindmap.type,
|
||||
},
|
||||
position: {
|
||||
x: mindmap.x,
|
||||
y: mindmap.y,
|
||||
x: mindmap.x!,
|
||||
y: mindmap.y!,
|
||||
},
|
||||
});
|
||||
if (mindmap.children) {
|
||||
@@ -88,12 +99,7 @@ function addNodes(mindmap, cy, conf, level) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param node
|
||||
* @param conf
|
||||
* @param cy
|
||||
*/
|
||||
function layoutMindmap(node, conf) {
|
||||
function layoutMindmap(node: MindmapNode, conf: MermaidConfig): Promise<cytoscape.Core> {
|
||||
return new Promise((resolve) => {
|
||||
// Add temporary render element
|
||||
const renderEl = select('body').append('div').attr('id', 'cy').attr('style', 'display:none');
|
||||
@@ -122,8 +128,8 @@ function layoutMindmap(node, conf) {
|
||||
|
||||
cy.layout({
|
||||
name: 'cose-bilkent',
|
||||
// @ts-ignore Types for cose-bilkent are not correct?
|
||||
quality: 'proof',
|
||||
// headless: true,
|
||||
styleEnabled: false,
|
||||
animate: false,
|
||||
}).run();
|
||||
@@ -133,18 +139,13 @@ function layoutMindmap(node, conf) {
|
||||
});
|
||||
});
|
||||
}
|
||||
/**
|
||||
* @param node
|
||||
* @param cy
|
||||
* @param positionedMindmap
|
||||
* @param conf
|
||||
*/
|
||||
function positionNodes(cy) {
|
||||
|
||||
function positionNodes(db: MindmapDB, cy: cytoscape.Core) {
|
||||
cy.nodes().map((node, id) => {
|
||||
const data = node.data();
|
||||
data.x = node.position().x;
|
||||
data.y = node.position().y;
|
||||
svgDraw.positionNode(data);
|
||||
positionNode(db, data);
|
||||
const el = db.getElementById(data.nodeId);
|
||||
log.info('Id:', id, 'Position: (', node.position().x, ', ', node.position().y, ')', data);
|
||||
el.attr(
|
||||
@@ -155,38 +156,19 @@ function positionNodes(cy) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws a an info picture in the tag with id: id based on the graph definition in text.
|
||||
*
|
||||
* @param {any} text
|
||||
* @param {any} id
|
||||
* @param {any} version
|
||||
* @param diagObj
|
||||
*/
|
||||
export const draw: DrawDefinition = async (text, id, _version, diagObj) => {
|
||||
log.debug('Rendering mindmap diagram\n' + text);
|
||||
|
||||
const db = diagObj.db as MindmapDB;
|
||||
const mm = db.getMindmap();
|
||||
if (!mm) {
|
||||
return;
|
||||
}
|
||||
|
||||
export const draw = async (text, id, version, diagObj) => {
|
||||
const conf = getConfig();
|
||||
|
||||
conf.htmlLabels = false;
|
||||
|
||||
log.debug('Rendering mindmap diagram\n' + text, diagObj.parser);
|
||||
|
||||
const securityLevel = getConfig().securityLevel;
|
||||
// Handle root and Document for when rendering in sandbox mode
|
||||
let sandboxElement;
|
||||
if (securityLevel === 'sandbox') {
|
||||
sandboxElement = select('#i' + id);
|
||||
}
|
||||
const root =
|
||||
securityLevel === 'sandbox'
|
||||
? select(sandboxElement.nodes()[0].contentDocument.body)
|
||||
: select('body');
|
||||
// Parse the graph definition
|
||||
|
||||
const svg = root.select('#' + id);
|
||||
|
||||
svg.append('g');
|
||||
const mm = diagObj.db.getMindmap();
|
||||
const svg = selectSvgElement(id);
|
||||
|
||||
// Draw the graph and start with drawing the nodes without proper position
|
||||
// this gives us the size of the nodes and we can set the positions later
|
||||
@@ -195,18 +177,23 @@ export const draw = async (text, id, version, diagObj) => {
|
||||
edgesElem.attr('class', 'mindmap-edges');
|
||||
const nodesElem = svg.append('g');
|
||||
nodesElem.attr('class', 'mindmap-nodes');
|
||||
drawNodes(nodesElem, mm, -1, conf);
|
||||
drawNodes(db, nodesElem, mm as FilledMindMapNode, -1, conf);
|
||||
|
||||
// Next step is to layout the mindmap, giving each node a position
|
||||
|
||||
const cy = await layoutMindmap(mm, conf);
|
||||
|
||||
// // After this we can draw, first the edges and the then nodes with the correct position
|
||||
drawEdges(edgesElem, cy, conf);
|
||||
positionNodes(cy, conf);
|
||||
// After this we can draw, first the edges and the then nodes with the correct position
|
||||
drawEdges(edgesElem, cy);
|
||||
positionNodes(db, cy);
|
||||
|
||||
// Setup the view box and size of the svg element
|
||||
setupGraphViewbox(undefined, svg, conf.mindmap.padding, conf.mindmap.useMaxWidth);
|
||||
setupGraphViewbox(
|
||||
undefined,
|
||||
svg,
|
||||
conf.mindmap?.padding ?? defaultConfig.mindmap.padding,
|
||||
conf.mindmap?.useMaxWidth ?? defaultConfig.mindmap.useMaxWidth
|
||||
);
|
||||
};
|
||||
|
||||
export default {
|
||||
22
packages/mermaid/src/diagrams/mindmap/mindmapTypes.ts
Normal file
22
packages/mermaid/src/diagrams/mindmap/mindmapTypes.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import type { RequiredDeep } from 'type-fest';
|
||||
import type mindmapDb from './mindmapDb.js';
|
||||
|
||||
export interface MindmapNode {
|
||||
id: number;
|
||||
nodeId: string;
|
||||
level: number;
|
||||
descr: string;
|
||||
type: number;
|
||||
children: MindmapNode[];
|
||||
width: number;
|
||||
padding: number;
|
||||
section?: number;
|
||||
height?: number;
|
||||
class?: string;
|
||||
icon?: string;
|
||||
x?: number;
|
||||
y?: number;
|
||||
}
|
||||
|
||||
export type FilledMindMapNode = RequiredDeep<MindmapNode>;
|
||||
export type MindmapDB = typeof mindmapDb;
|
||||
@@ -1,6 +1,8 @@
|
||||
// @ts-expect-error Incorrect khroma types
|
||||
import { darken, lighten, isDark } from 'khroma';
|
||||
import type { DiagramStylesProvider } from '../../diagram-api/types.js';
|
||||
|
||||
const genSections = (options) => {
|
||||
const genSections: DiagramStylesProvider = (options) => {
|
||||
let sections = '';
|
||||
|
||||
for (let i = 0; i < options.THEME_COLOR_LIMIT; i++) {
|
||||
@@ -49,7 +51,8 @@ const genSections = (options) => {
|
||||
return sections;
|
||||
};
|
||||
|
||||
const getStyles = (options) =>
|
||||
// TODO: These options seem incorrect.
|
||||
const getStyles: DiagramStylesProvider = (options) =>
|
||||
`
|
||||
.edge {
|
||||
stroke-width: 3;
|
||||
@@ -1,55 +1,20 @@
|
||||
import { select } from 'd3';
|
||||
import * as db from './mindmapDb.js';
|
||||
import type { D3Element } from '../../mermaidAPI.js';
|
||||
import { createText } from '../../rendering-util/createText.js';
|
||||
import type { FilledMindMapNode, MindmapDB } from './mindmapTypes.js';
|
||||
import type { Point } from '../../types.js';
|
||||
import { parseFontSize } from '../../utils.js';
|
||||
import type { MermaidConfig } from '../../config.type.js';
|
||||
|
||||
const MAX_SECTIONS = 12;
|
||||
|
||||
/**
|
||||
* @param {string} text The text to be wrapped
|
||||
* @param {number} width The max width of the text
|
||||
*/
|
||||
function wrap(text, width) {
|
||||
text.each(function () {
|
||||
var text = select(this),
|
||||
words = text
|
||||
.text()
|
||||
.split(/(\s+|<br\/>)/)
|
||||
.reverse(),
|
||||
word,
|
||||
line = [],
|
||||
lineHeight = 1.1, // ems
|
||||
y = text.attr('y'),
|
||||
dy = parseFloat(text.attr('dy')),
|
||||
tspan = text
|
||||
.text(null)
|
||||
.append('tspan')
|
||||
.attr('x', 0)
|
||||
.attr('y', y)
|
||||
.attr('dy', dy + 'em');
|
||||
for (let j = 0; j < words.length; j++) {
|
||||
word = words[words.length - 1 - j];
|
||||
line.push(word);
|
||||
tspan.text(line.join(' ').trim());
|
||||
if (tspan.node().getComputedTextLength() > width || word === '<br/>') {
|
||||
line.pop();
|
||||
tspan.text(line.join(' ').trim());
|
||||
if (word === '<br/>') {
|
||||
line = [''];
|
||||
} else {
|
||||
line = [word];
|
||||
}
|
||||
type ShapeFunction = (
|
||||
db: MindmapDB,
|
||||
elem: D3Element,
|
||||
node: FilledMindMapNode,
|
||||
section?: number
|
||||
) => void;
|
||||
|
||||
tspan = text
|
||||
.append('tspan')
|
||||
.attr('x', 0)
|
||||
.attr('y', y)
|
||||
.attr('dy', lineHeight + 'em')
|
||||
.text(word);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const defaultBkg = function (elem, node, section) {
|
||||
const defaultBkg: ShapeFunction = function (db, elem, node, section) {
|
||||
const rd = 5;
|
||||
elem
|
||||
.append('path')
|
||||
@@ -71,7 +36,7 @@ const defaultBkg = function (elem, node, section) {
|
||||
.attr('y2', node.height);
|
||||
};
|
||||
|
||||
const rectBkg = function (elem, node) {
|
||||
const rectBkg: ShapeFunction = function (db, elem, node) {
|
||||
elem
|
||||
.append('rect')
|
||||
.attr('id', 'node-' + node.id)
|
||||
@@ -80,7 +45,7 @@ const rectBkg = function (elem, node) {
|
||||
.attr('width', node.width);
|
||||
};
|
||||
|
||||
const cloudBkg = function (elem, node) {
|
||||
const cloudBkg: ShapeFunction = function (db, elem, node) {
|
||||
const w = node.width;
|
||||
const h = node.height;
|
||||
const r1 = 0.15 * w;
|
||||
@@ -111,7 +76,7 @@ const cloudBkg = function (elem, node) {
|
||||
);
|
||||
};
|
||||
|
||||
const bangBkg = function (elem, node) {
|
||||
const bangBkg: ShapeFunction = function (db, elem, node) {
|
||||
const w = node.width;
|
||||
const h = node.height;
|
||||
const r = 0.15 * w;
|
||||
@@ -143,7 +108,7 @@ const bangBkg = function (elem, node) {
|
||||
);
|
||||
};
|
||||
|
||||
const circleBkg = function (elem, node) {
|
||||
const circleBkg: ShapeFunction = function (db, elem, node) {
|
||||
elem
|
||||
.append('circle')
|
||||
.attr('id', 'node-' + node.id)
|
||||
@@ -151,15 +116,13 @@ const circleBkg = function (elem, node) {
|
||||
.attr('r', node.width / 2);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param parent
|
||||
* @param w
|
||||
* @param h
|
||||
* @param points
|
||||
* @param node
|
||||
*/
|
||||
function insertPolygonShape(parent, w, h, points, node) {
|
||||
function insertPolygonShape(
|
||||
parent: D3Element,
|
||||
w: number,
|
||||
h: number,
|
||||
points: Point[],
|
||||
node: FilledMindMapNode
|
||||
) {
|
||||
return parent
|
||||
.insert('polygon', ':first-child')
|
||||
.attr(
|
||||
@@ -173,12 +136,16 @@ function insertPolygonShape(parent, w, h, points, node) {
|
||||
.attr('transform', 'translate(' + (node.width - w) / 2 + ', ' + h + ')');
|
||||
}
|
||||
|
||||
const hexagonBkg = function (elem, node) {
|
||||
const hexagonBkg: ShapeFunction = function (
|
||||
_db: MindmapDB,
|
||||
elem: D3Element,
|
||||
node: FilledMindMapNode
|
||||
) {
|
||||
const h = node.height;
|
||||
const f = 4;
|
||||
const m = h / f;
|
||||
const w = node.width - node.padding + 2 * m;
|
||||
const points = [
|
||||
const points: Point[] = [
|
||||
{ x: m, y: 0 },
|
||||
{ x: w - m, y: 0 },
|
||||
{ x: w, y: -h / 2 },
|
||||
@@ -186,10 +153,10 @@ const hexagonBkg = function (elem, node) {
|
||||
{ x: m, y: -h },
|
||||
{ x: 0, y: -h / 2 },
|
||||
];
|
||||
const shapeSvg = insertPolygonShape(elem, w, h, points, node);
|
||||
insertPolygonShape(elem, w, h, points, node);
|
||||
};
|
||||
|
||||
const roundedRectBkg = function (elem, node) {
|
||||
const roundedRectBkg: ShapeFunction = function (db, elem, node) {
|
||||
elem
|
||||
.append('rect')
|
||||
.attr('id', 'node-' + node.id)
|
||||
@@ -201,13 +168,20 @@ const roundedRectBkg = function (elem, node) {
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {object} elem The D3 dom element in which the node is to be added
|
||||
* @param {object} node The node to be added
|
||||
* @param fullSection
|
||||
* @param {object} conf The configuration object
|
||||
* @returns {number} The height nodes dom element
|
||||
* @param db - The database
|
||||
* @param elem - The D3 dom element in which the node is to be added
|
||||
* @param node - The node to be added
|
||||
* @param fullSection - ?
|
||||
* @param conf - The configuration object
|
||||
* @returns The height nodes dom element
|
||||
*/
|
||||
export const drawNode = function (elem, node, fullSection, conf) {
|
||||
export const drawNode = function (
|
||||
db: MindmapDB,
|
||||
elem: D3Element,
|
||||
node: FilledMindMapNode,
|
||||
fullSection: number,
|
||||
conf: MermaidConfig
|
||||
): number {
|
||||
const htmlLabels = conf.htmlLabels;
|
||||
const section = fullSection % (MAX_SECTIONS - 1);
|
||||
const nodeElem = elem.append('g');
|
||||
@@ -235,10 +209,9 @@ export const drawNode = function (elem, node, fullSection, conf) {
|
||||
.attr('dominant-baseline', 'middle')
|
||||
.attr('text-anchor', 'middle');
|
||||
}
|
||||
// .call(wrap, node.width);
|
||||
const bbox = textElem.node().getBBox();
|
||||
const fontSize = conf.fontSize.replace ? conf.fontSize.replace('px', '') : conf.fontSize;
|
||||
node.height = bbox.height + fontSize * 1.1 * 0.5 + node.padding;
|
||||
const [fontSize] = parseFontSize(conf.fontSize);
|
||||
node.height = bbox.height + fontSize! * 1.1 * 0.5 + node.padding;
|
||||
node.width = bbox.width + 2 * node.padding;
|
||||
if (node.icon) {
|
||||
if (node.type === db.nodeType.CIRCLE) {
|
||||
@@ -294,60 +267,34 @@ export const drawNode = function (elem, node, fullSection, conf) {
|
||||
|
||||
switch (node.type) {
|
||||
case db.nodeType.DEFAULT:
|
||||
defaultBkg(bkgElem, node, section, conf);
|
||||
defaultBkg(db, bkgElem, node, section);
|
||||
break;
|
||||
case db.nodeType.ROUNDED_RECT:
|
||||
roundedRectBkg(bkgElem, node, section, conf);
|
||||
roundedRectBkg(db, bkgElem, node, section);
|
||||
break;
|
||||
case db.nodeType.RECT:
|
||||
rectBkg(bkgElem, node, section, conf);
|
||||
rectBkg(db, bkgElem, node, section);
|
||||
break;
|
||||
case db.nodeType.CIRCLE:
|
||||
bkgElem.attr('transform', 'translate(' + node.width / 2 + ', ' + +node.height / 2 + ')');
|
||||
circleBkg(bkgElem, node, section, conf);
|
||||
circleBkg(db, bkgElem, node, section);
|
||||
break;
|
||||
case db.nodeType.CLOUD:
|
||||
cloudBkg(bkgElem, node, section, conf);
|
||||
cloudBkg(db, bkgElem, node, section);
|
||||
break;
|
||||
case db.nodeType.BANG:
|
||||
bangBkg(bkgElem, node, section, conf);
|
||||
bangBkg(db, bkgElem, node, section);
|
||||
break;
|
||||
case db.nodeType.HEXAGON:
|
||||
hexagonBkg(bkgElem, node, section, conf);
|
||||
hexagonBkg(db, bkgElem, node, section);
|
||||
break;
|
||||
}
|
||||
|
||||
// Position the node to its coordinate
|
||||
// if (typeof node.x !== 'undefined' && typeof node.y !== 'undefined') {
|
||||
// nodeElem.attr('transform', 'translate(' + node.x + ',' + node.y + ')');
|
||||
// }
|
||||
db.setElementForId(node.id, nodeElem);
|
||||
return node.height;
|
||||
};
|
||||
|
||||
export const drawEdge = function drawEdge(edgesElem, mindmap, parent, depth, fullSection) {
|
||||
const section = fullSection % (MAX_SECTIONS - 1);
|
||||
const sx = parent.x + parent.width / 2;
|
||||
const sy = parent.y + parent.height / 2;
|
||||
const ex = mindmap.x + mindmap.width / 2;
|
||||
const ey = mindmap.y + mindmap.height / 2;
|
||||
const mx = ex > sx ? sx + Math.abs(sx - ex) / 2 : sx - Math.abs(sx - ex) / 2;
|
||||
const my = ey > sy ? sy + Math.abs(sy - ey) / 2 : sy - Math.abs(sy - ey) / 2;
|
||||
const qx = ex > sx ? Math.abs(sx - mx) / 2 + sx : -Math.abs(sx - mx) / 2 + sx;
|
||||
const qy = ey > sy ? Math.abs(sy - my) / 2 + sy : -Math.abs(sy - my) / 2 + sy;
|
||||
|
||||
edgesElem
|
||||
.append('path')
|
||||
.attr(
|
||||
'd',
|
||||
parent.direction === 'TB' || parent.direction === 'BT'
|
||||
? `M${sx},${sy} Q${sx},${qy} ${mx},${my} T${ex},${ey}`
|
||||
: `M${sx},${sy} Q${qx},${sy} ${mx},${my} T${ex},${ey}`
|
||||
)
|
||||
.attr('class', 'edge section-edge-' + section + ' edge-depth-' + depth);
|
||||
};
|
||||
|
||||
export const positionNode = function (node) {
|
||||
export const positionNode = function (db: MindmapDB, node: FilledMindMapNode) {
|
||||
const nodeElem = db.getElementById(node.id);
|
||||
|
||||
const x = node.x || 0;
|
||||
@@ -355,5 +302,3 @@ export const positionNode = function (node) {
|
||||
// Position the node to its coordinate
|
||||
nodeElem.attr('transform', 'translate(' + x + ',' + y + ')');
|
||||
};
|
||||
|
||||
export default { drawNode, positionNode, drawEdge };
|
||||
59
packages/mermaid/src/diagrams/packet/db.ts
Normal file
59
packages/mermaid/src/diagrams/packet/db.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { getConfig as commonGetConfig } from '../../config.js';
|
||||
import type { PacketDiagramConfig } from '../../config.type.js';
|
||||
import DEFAULT_CONFIG from '../../defaultConfig.js';
|
||||
import { cleanAndMerge } from '../../utils.js';
|
||||
import {
|
||||
clear as commonClear,
|
||||
getAccDescription,
|
||||
getAccTitle,
|
||||
getDiagramTitle,
|
||||
setAccDescription,
|
||||
setAccTitle,
|
||||
setDiagramTitle,
|
||||
} from '../common/commonDb.js';
|
||||
import type { PacketDB, PacketData, PacketWord } from './types.js';
|
||||
|
||||
const defaultPacketData: PacketData = {
|
||||
packet: [],
|
||||
};
|
||||
|
||||
let data: PacketData = structuredClone(defaultPacketData);
|
||||
|
||||
const DEFAULT_PACKET_CONFIG: Required<PacketDiagramConfig> = DEFAULT_CONFIG.packet;
|
||||
|
||||
const getConfig = (): Required<PacketDiagramConfig> => {
|
||||
const config = cleanAndMerge({
|
||||
...DEFAULT_PACKET_CONFIG,
|
||||
...commonGetConfig().packet,
|
||||
});
|
||||
if (config.showBits) {
|
||||
config.paddingY += 10;
|
||||
}
|
||||
return config;
|
||||
};
|
||||
|
||||
const getPacket = (): PacketWord[] => data.packet;
|
||||
|
||||
const pushWord = (word: PacketWord) => {
|
||||
if (word.length > 0) {
|
||||
data.packet.push(word);
|
||||
}
|
||||
};
|
||||
|
||||
const clear = () => {
|
||||
commonClear();
|
||||
data = structuredClone(defaultPacketData);
|
||||
};
|
||||
|
||||
export const db: PacketDB = {
|
||||
pushWord,
|
||||
getPacket,
|
||||
getConfig,
|
||||
clear,
|
||||
setAccTitle,
|
||||
getAccTitle,
|
||||
setDiagramTitle,
|
||||
getDiagramTitle,
|
||||
getAccDescription,
|
||||
setAccDescription,
|
||||
};
|
||||
22
packages/mermaid/src/diagrams/packet/detector.ts
Normal file
22
packages/mermaid/src/diagrams/packet/detector.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import type {
|
||||
DiagramDetector,
|
||||
DiagramLoader,
|
||||
ExternalDiagramDefinition,
|
||||
} from '../../diagram-api/types.js';
|
||||
|
||||
const id = 'packet';
|
||||
|
||||
const detector: DiagramDetector = (txt) => {
|
||||
return /^\s*packet-beta/.test(txt);
|
||||
};
|
||||
|
||||
const loader: DiagramLoader = async () => {
|
||||
const { diagram } = await import('./diagram.js');
|
||||
return { id, diagram };
|
||||
};
|
||||
|
||||
export const packet: ExternalDiagramDefinition = {
|
||||
id,
|
||||
detector,
|
||||
loader,
|
||||
};
|
||||
12
packages/mermaid/src/diagrams/packet/diagram.ts
Normal file
12
packages/mermaid/src/diagrams/packet/diagram.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import type { DiagramDefinition } from '../../diagram-api/types.js';
|
||||
import { db } from './db.js';
|
||||
import { parser } from './parser.js';
|
||||
import { renderer } from './renderer.js';
|
||||
import { styles } from './styles.js';
|
||||
|
||||
export const diagram: DiagramDefinition = {
|
||||
parser,
|
||||
db,
|
||||
renderer,
|
||||
styles,
|
||||
};
|
||||
175
packages/mermaid/src/diagrams/packet/packet.spec.ts
Normal file
175
packages/mermaid/src/diagrams/packet/packet.spec.ts
Normal file
@@ -0,0 +1,175 @@
|
||||
import { it, describe, expect } from 'vitest';
|
||||
import { db } from './db.js';
|
||||
import { parser } from './parser.js';
|
||||
|
||||
const { clear, getPacket, getDiagramTitle, getAccTitle, getAccDescription } = db;
|
||||
|
||||
describe('packet diagrams', () => {
|
||||
beforeEach(() => {
|
||||
clear();
|
||||
});
|
||||
|
||||
it('should handle a packet-beta definition', async () => {
|
||||
const str = `packet-beta`;
|
||||
await expect(parser.parse(str)).resolves.not.toThrow();
|
||||
expect(getPacket()).toMatchInlineSnapshot('[]');
|
||||
});
|
||||
|
||||
it('should handle diagram with data and title', async () => {
|
||||
const str = `packet-beta
|
||||
title Packet diagram
|
||||
accTitle: Packet accTitle
|
||||
accDescr: Packet accDescription
|
||||
0-10: "test"
|
||||
`;
|
||||
await expect(parser.parse(str)).resolves.not.toThrow();
|
||||
expect(getDiagramTitle()).toMatchInlineSnapshot('"Packet diagram"');
|
||||
expect(getAccTitle()).toMatchInlineSnapshot('"Packet accTitle"');
|
||||
expect(getAccDescription()).toMatchInlineSnapshot('"Packet accDescription"');
|
||||
expect(getPacket()).toMatchInlineSnapshot(`
|
||||
[
|
||||
[
|
||||
{
|
||||
"end": 10,
|
||||
"label": "test",
|
||||
"start": 0,
|
||||
},
|
||||
],
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('should handle single bits', async () => {
|
||||
const str = `packet-beta
|
||||
0-10: "test"
|
||||
11: "single"
|
||||
`;
|
||||
await expect(parser.parse(str)).resolves.not.toThrow();
|
||||
expect(getPacket()).toMatchInlineSnapshot(`
|
||||
[
|
||||
[
|
||||
{
|
||||
"end": 10,
|
||||
"label": "test",
|
||||
"start": 0,
|
||||
},
|
||||
{
|
||||
"end": 11,
|
||||
"label": "single",
|
||||
"start": 11,
|
||||
},
|
||||
],
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('should split into multiple rows', async () => {
|
||||
const str = `packet-beta
|
||||
0-10: "test"
|
||||
11-90: "multiple"
|
||||
`;
|
||||
await expect(parser.parse(str)).resolves.not.toThrow();
|
||||
expect(getPacket()).toMatchInlineSnapshot(`
|
||||
[
|
||||
[
|
||||
{
|
||||
"end": 10,
|
||||
"label": "test",
|
||||
"start": 0,
|
||||
},
|
||||
{
|
||||
"end": 31,
|
||||
"label": "multiple",
|
||||
"start": 11,
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
"end": 63,
|
||||
"label": "multiple",
|
||||
"start": 32,
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
"end": 90,
|
||||
"label": "multiple",
|
||||
"start": 64,
|
||||
},
|
||||
],
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('should split into multiple rows when cut at exact length', async () => {
|
||||
const str = `packet-beta
|
||||
0-16: "test"
|
||||
17-63: "multiple"
|
||||
`;
|
||||
await expect(parser.parse(str)).resolves.not.toThrow();
|
||||
expect(getPacket()).toMatchInlineSnapshot(`
|
||||
[
|
||||
[
|
||||
{
|
||||
"end": 16,
|
||||
"label": "test",
|
||||
"start": 0,
|
||||
},
|
||||
{
|
||||
"end": 31,
|
||||
"label": "multiple",
|
||||
"start": 17,
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
"end": 63,
|
||||
"label": "multiple",
|
||||
"start": 32,
|
||||
},
|
||||
],
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('should throw error if numbers are not continuous', async () => {
|
||||
const str = `packet-beta
|
||||
0-16: "test"
|
||||
18-20: "error"
|
||||
`;
|
||||
await expect(parser.parse(str)).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
'"Packet block 18 - 20 is not contiguous. It should start from 17."'
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error if numbers are not continuous for single packets', async () => {
|
||||
const str = `packet-beta
|
||||
0-16: "test"
|
||||
18: "error"
|
||||
`;
|
||||
await expect(parser.parse(str)).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
'"Packet block 18 - 18 is not contiguous. It should start from 17."'
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error if numbers are not continuous for single packets - 2', async () => {
|
||||
const str = `packet-beta
|
||||
0-16: "test"
|
||||
17: "good"
|
||||
19: "error"
|
||||
`;
|
||||
await expect(parser.parse(str)).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
'"Packet block 19 - 19 is not contiguous. It should start from 18."'
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error if end is less than start', async () => {
|
||||
const str = `packet-beta
|
||||
0-16: "test"
|
||||
25-20: "error"
|
||||
`;
|
||||
await expect(parser.parse(str)).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
'"Packet block 25 - 20 is invalid. End must be greater than start."'
|
||||
);
|
||||
});
|
||||
});
|
||||
85
packages/mermaid/src/diagrams/packet/parser.ts
Normal file
85
packages/mermaid/src/diagrams/packet/parser.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import type { Packet } from '@mermaid-js/parser';
|
||||
import { parse } from '@mermaid-js/parser';
|
||||
import type { ParserDefinition } from '../../diagram-api/types.js';
|
||||
import { log } from '../../logger.js';
|
||||
import { populateCommonDb } from '../common/populateCommonDb.js';
|
||||
import { db } from './db.js';
|
||||
import type { PacketBlock, PacketWord } from './types.js';
|
||||
|
||||
const maxPacketSize = 10_000;
|
||||
|
||||
const populate = (ast: Packet) => {
|
||||
populateCommonDb(ast, db);
|
||||
let lastByte = -1;
|
||||
let word: PacketWord = [];
|
||||
let row = 1;
|
||||
const { bitsPerRow } = db.getConfig();
|
||||
for (let { start, end, label } of ast.blocks) {
|
||||
if (end && end < start) {
|
||||
throw new Error(`Packet block ${start} - ${end} is invalid. End must be greater than start.`);
|
||||
}
|
||||
if (start !== lastByte + 1) {
|
||||
throw new Error(
|
||||
`Packet block ${start} - ${end ?? start} is not contiguous. It should start from ${
|
||||
lastByte + 1
|
||||
}.`
|
||||
);
|
||||
}
|
||||
lastByte = end ?? start;
|
||||
log.debug(`Packet block ${start} - ${lastByte} with label ${label}`);
|
||||
|
||||
while (word.length <= bitsPerRow + 1 && db.getPacket().length < maxPacketSize) {
|
||||
const [block, nextBlock] = getNextFittingBlock({ start, end, label }, row, bitsPerRow);
|
||||
word.push(block);
|
||||
if (block.end + 1 === row * bitsPerRow) {
|
||||
db.pushWord(word);
|
||||
word = [];
|
||||
row++;
|
||||
}
|
||||
if (!nextBlock) {
|
||||
break;
|
||||
}
|
||||
({ start, end, label } = nextBlock);
|
||||
}
|
||||
}
|
||||
db.pushWord(word);
|
||||
};
|
||||
|
||||
const getNextFittingBlock = (
|
||||
block: PacketBlock,
|
||||
row: number,
|
||||
bitsPerRow: number
|
||||
): [Required<PacketBlock>, PacketBlock | undefined] => {
|
||||
if (block.end === undefined) {
|
||||
block.end = block.start;
|
||||
}
|
||||
|
||||
if (block.start > block.end) {
|
||||
throw new Error(`Block start ${block.start} is greater than block end ${block.end}.`);
|
||||
}
|
||||
|
||||
if (block.end + 1 <= row * bitsPerRow) {
|
||||
return [block as Required<PacketBlock>, undefined];
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
start: block.start,
|
||||
end: row * bitsPerRow - 1,
|
||||
label: block.label,
|
||||
},
|
||||
{
|
||||
start: row * bitsPerRow,
|
||||
end: block.end,
|
||||
label: block.label,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export const parser: ParserDefinition = {
|
||||
parse: async (input: string): Promise<void> => {
|
||||
const ast: Packet = await parse('packet', input);
|
||||
log.debug(ast);
|
||||
populate(ast);
|
||||
},
|
||||
};
|
||||
95
packages/mermaid/src/diagrams/packet/renderer.ts
Normal file
95
packages/mermaid/src/diagrams/packet/renderer.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import type { Diagram } from '../../Diagram.js';
|
||||
import type { PacketDiagramConfig } from '../../config.type.js';
|
||||
import type { DiagramRenderer, DrawDefinition, Group, SVG } from '../../diagram-api/types.js';
|
||||
import { selectSvgElement } from '../../rendering-util/selectSvgElement.js';
|
||||
import { configureSvgSize } from '../../setupGraphViewbox.js';
|
||||
import type { PacketDB, PacketWord } from './types.js';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const draw: DrawDefinition = (_text, id, _version, diagram: Diagram) => {
|
||||
const db = diagram.db as PacketDB;
|
||||
const config = db.getConfig();
|
||||
const { rowHeight, paddingY, bitWidth, bitsPerRow } = config;
|
||||
const words = db.getPacket();
|
||||
const title = db.getDiagramTitle();
|
||||
const totalRowHeight = rowHeight + paddingY;
|
||||
const svgHeight = totalRowHeight * (words.length + 1) - (title ? 0 : rowHeight);
|
||||
const svgWidth = bitWidth * bitsPerRow + 2;
|
||||
const svg: SVG = selectSvgElement(id);
|
||||
|
||||
svg.attr('viewbox', `0 0 ${svgWidth} ${svgHeight}`);
|
||||
configureSvgSize(svg, svgHeight, svgWidth, config.useMaxWidth);
|
||||
|
||||
for (const [word, packet] of words.entries()) {
|
||||
drawWord(svg, packet, word, config);
|
||||
}
|
||||
|
||||
svg
|
||||
.append('text')
|
||||
.text(title)
|
||||
.attr('x', svgWidth / 2)
|
||||
.attr('y', svgHeight - totalRowHeight / 2)
|
||||
.attr('dominant-baseline', 'middle')
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('class', 'packetTitle');
|
||||
};
|
||||
|
||||
const drawWord = (
|
||||
svg: SVG,
|
||||
word: PacketWord,
|
||||
rowNumber: number,
|
||||
{ rowHeight, paddingX, paddingY, bitWidth, bitsPerRow, showBits }: Required<PacketDiagramConfig>
|
||||
) => {
|
||||
const group: Group = svg.append('g');
|
||||
const wordY = rowNumber * (rowHeight + paddingY) + paddingY;
|
||||
for (const block of word) {
|
||||
const blockX = (block.start % bitsPerRow) * bitWidth + 1;
|
||||
const width = (block.end - block.start + 1) * bitWidth - paddingX;
|
||||
// Block rectangle
|
||||
group
|
||||
.append('rect')
|
||||
.attr('x', blockX)
|
||||
.attr('y', wordY)
|
||||
.attr('width', width)
|
||||
.attr('height', rowHeight)
|
||||
.attr('class', 'packetBlock');
|
||||
|
||||
// Block label
|
||||
group
|
||||
.append('text')
|
||||
.attr('x', blockX + width / 2)
|
||||
.attr('y', wordY + rowHeight / 2)
|
||||
.attr('class', 'packetLabel')
|
||||
.attr('dominant-baseline', 'middle')
|
||||
.attr('text-anchor', 'middle')
|
||||
.text(block.label);
|
||||
|
||||
if (!showBits) {
|
||||
continue;
|
||||
}
|
||||
// Start byte count
|
||||
const isSingleBlock = block.end === block.start;
|
||||
const bitNumberY = wordY - 2;
|
||||
group
|
||||
.append('text')
|
||||
.attr('x', blockX + (isSingleBlock ? width / 2 : 0))
|
||||
.attr('y', bitNumberY)
|
||||
.attr('class', 'packetByte start')
|
||||
.attr('dominant-baseline', 'auto')
|
||||
.attr('text-anchor', isSingleBlock ? 'middle' : 'start')
|
||||
.text(block.start);
|
||||
|
||||
// Draw end byte count if it is not the same as start byte count
|
||||
if (!isSingleBlock) {
|
||||
group
|
||||
.append('text')
|
||||
.attr('x', blockX + width)
|
||||
.attr('y', bitNumberY)
|
||||
.attr('class', 'packetByte end')
|
||||
.attr('dominant-baseline', 'auto')
|
||||
.attr('text-anchor', 'end')
|
||||
.text(block.end);
|
||||
}
|
||||
}
|
||||
};
|
||||
export const renderer: DiagramRenderer = { draw };
|
||||
47
packages/mermaid/src/diagrams/packet/styles.ts
Normal file
47
packages/mermaid/src/diagrams/packet/styles.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import type { DiagramStylesProvider } from '../../diagram-api/types.js';
|
||||
import { cleanAndMerge } from '../../utils.js';
|
||||
import type { PacketStyleOptions } from './types.js';
|
||||
|
||||
const defaultPacketStyleOptions: PacketStyleOptions = {
|
||||
byteFontSize: '10px',
|
||||
startByteColor: 'black',
|
||||
endByteColor: 'black',
|
||||
labelColor: 'black',
|
||||
labelFontSize: '12px',
|
||||
titleColor: 'black',
|
||||
titleFontSize: '14px',
|
||||
blockStrokeColor: 'black',
|
||||
blockStrokeWidth: '1',
|
||||
blockFillColor: '#efefef',
|
||||
};
|
||||
|
||||
export const styles: DiagramStylesProvider = ({ packet }: { packet?: PacketStyleOptions } = {}) => {
|
||||
const options = cleanAndMerge(defaultPacketStyleOptions, packet);
|
||||
|
||||
return `
|
||||
.packetByte {
|
||||
font-size: ${options.byteFontSize};
|
||||
}
|
||||
.packetByte.start {
|
||||
fill: ${options.startByteColor};
|
||||
}
|
||||
.packetByte.end {
|
||||
fill: ${options.endByteColor};
|
||||
}
|
||||
.packetLabel {
|
||||
fill: ${options.labelColor};
|
||||
font-size: ${options.labelFontSize};
|
||||
}
|
||||
.packetTitle {
|
||||
fill: ${options.titleColor};
|
||||
font-size: ${options.titleFontSize};
|
||||
}
|
||||
.packetBlock {
|
||||
stroke: ${options.blockStrokeColor};
|
||||
stroke-width: ${options.blockStrokeWidth};
|
||||
fill: ${options.blockFillColor};
|
||||
}
|
||||
`;
|
||||
};
|
||||
|
||||
export default styles;
|
||||
29
packages/mermaid/src/diagrams/packet/types.ts
Normal file
29
packages/mermaid/src/diagrams/packet/types.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import type { Packet, RecursiveAstOmit } from '@mermaid-js/parser';
|
||||
import type { PacketDiagramConfig } from '../../config.type.js';
|
||||
import type { DiagramDBBase } from '../../diagram-api/types.js';
|
||||
import type { ArrayElement } from '../../types.js';
|
||||
|
||||
export type PacketBlock = RecursiveAstOmit<ArrayElement<Packet['blocks']>>;
|
||||
export type PacketWord = Required<PacketBlock>[];
|
||||
|
||||
export interface PacketDB extends DiagramDBBase<PacketDiagramConfig> {
|
||||
pushWord: (word: PacketWord) => void;
|
||||
getPacket: () => PacketWord[];
|
||||
}
|
||||
|
||||
export interface PacketStyleOptions {
|
||||
byteFontSize?: string;
|
||||
startByteColor?: string;
|
||||
endByteColor?: string;
|
||||
labelColor?: string;
|
||||
labelFontSize?: string;
|
||||
blockStrokeColor?: string;
|
||||
blockStrokeWidth?: string;
|
||||
blockFillColor?: string;
|
||||
titleColor?: string;
|
||||
titleFontSize?: string;
|
||||
}
|
||||
|
||||
export interface PacketData {
|
||||
packet: PacketWord[];
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user