mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-11-02 11:54:15 +01:00
@@ -37,14 +37,14 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@braintree/sanitize-url": "^7.0.0",
|
||||
"@braintree/sanitize-url": "^7.0.4",
|
||||
"d3": "^7.9.0",
|
||||
"khroma": "^2.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"concurrently": "^8.2.2",
|
||||
"concurrently": "^9.1.2",
|
||||
"mermaid": "workspace:*",
|
||||
"rimraf": "^5.0.5"
|
||||
"rimraf": "^6.0.1"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
"mermaid": "workspace:^"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"mermaid": "^11.0.0"
|
||||
"mermaid": "^11.0.2"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@zenuml/core": "^3.23.27"
|
||||
"@zenuml/core": "^3.23.28"
|
||||
},
|
||||
"devDependencies": {
|
||||
"mermaid": "workspace:^"
|
||||
|
||||
@@ -67,68 +67,67 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@braintree/sanitize-url": "^7.0.1",
|
||||
"@iconify/utils": "^2.1.32",
|
||||
"@braintree/sanitize-url": "^7.0.4",
|
||||
"@iconify/utils": "^2.1.33",
|
||||
"@mermaid-js/parser": "workspace:^",
|
||||
"@types/d3": "^7.4.3",
|
||||
"cytoscape": "^3.29.2",
|
||||
"cytoscape": "^3.29.3",
|
||||
"cytoscape-cose-bilkent": "^4.1.0",
|
||||
"cytoscape-fcose": "^2.2.0",
|
||||
"d3": "^7.9.0",
|
||||
"d3-sankey": "^0.12.3",
|
||||
"dagre-d3-es": "7.0.11",
|
||||
"dayjs": "^1.11.10",
|
||||
"dompurify": "^3.2.1",
|
||||
"dayjs": "^1.11.13",
|
||||
"dompurify": "^3.2.4",
|
||||
"katex": "^0.16.9",
|
||||
"khroma": "^2.1.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"marked": "^13.0.2",
|
||||
"marked": "^15.0.7",
|
||||
"roughjs": "^4.6.6",
|
||||
"stylis": "^4.3.1",
|
||||
"stylis": "^4.3.6",
|
||||
"ts-dedent": "^2.2.0",
|
||||
"uuid": "^9.0.1"
|
||||
"uuid": "^11.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@adobe/jsonschema2md": "^8.0.0",
|
||||
"@adobe/jsonschema2md": "^8.0.2",
|
||||
"@iconify/types": "^2.0.0",
|
||||
"@types/cytoscape": "^3.21.4",
|
||||
"@types/cytoscape": "^3.21.9",
|
||||
"@types/cytoscape-fcose": "^2.2.4",
|
||||
"@types/d3-sankey": "^0.12.4",
|
||||
"@types/d3-scale": "^4.0.8",
|
||||
"@types/d3-scale-chromatic": "^3.0.3",
|
||||
"@types/d3-selection": "^3.0.10",
|
||||
"@types/d3-shape": "^3.1.6",
|
||||
"@types/jsdom": "^21.1.6",
|
||||
"@types/d3-scale": "^4.0.9",
|
||||
"@types/d3-scale-chromatic": "^3.1.0",
|
||||
"@types/d3-selection": "^3.0.11",
|
||||
"@types/d3-shape": "^3.1.7",
|
||||
"@types/jsdom": "^21.1.7",
|
||||
"@types/katex": "^0.16.7",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/micromatch": "^4.0.6",
|
||||
"@types/prettier": "^3.0.0",
|
||||
"@types/stylis": "^4.2.5",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"ajv": "^8.12.0",
|
||||
"chokidar": "^3.6.0",
|
||||
"concurrently": "^8.2.2",
|
||||
"csstree-validator": "^3.0.0",
|
||||
"globby": "^14.0.1",
|
||||
"@types/micromatch": "^4.0.9",
|
||||
"@types/stylis": "^4.2.7",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"ajv": "^8.17.1",
|
||||
"chokidar": "^4.0.3",
|
||||
"concurrently": "^9.1.2",
|
||||
"csstree-validator": "^4.0.1",
|
||||
"globby": "^14.0.2",
|
||||
"jison": "^0.4.18",
|
||||
"js-base64": "^3.7.7",
|
||||
"jsdom": "^24.0.0",
|
||||
"json-schema-to-typescript": "^13.1.2",
|
||||
"micromatch": "^4.0.5",
|
||||
"jsdom": "^26.0.0",
|
||||
"json-schema-to-typescript": "^15.0.4",
|
||||
"micromatch": "^4.0.8",
|
||||
"path-browserify": "^1.0.1",
|
||||
"prettier": "^3.2.5",
|
||||
"prettier": "^3.5.2",
|
||||
"remark": "^15.0.1",
|
||||
"remark-frontmatter": "^5.0.0",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"rimraf": "^5.0.5",
|
||||
"start-server-and-test": "^2.0.3",
|
||||
"type-fest": "^4.13.1",
|
||||
"typedoc": "^0.25.12",
|
||||
"typedoc-plugin-markdown": "^3.17.1",
|
||||
"typescript": "~5.4.3",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"rimraf": "^6.0.1",
|
||||
"start-server-and-test": "^2.0.10",
|
||||
"type-fest": "^4.35.0",
|
||||
"typedoc": "^0.27.8",
|
||||
"typedoc-plugin-markdown": "^4.4.2",
|
||||
"typescript": "~5.7.3",
|
||||
"unist-util-flatmap": "^1.0.0",
|
||||
"unist-util-visit": "^5.0.0",
|
||||
"vitepress": "^1.0.1",
|
||||
"vitepress": "^1.0.2",
|
||||
"vitepress-plugin-search": "1.0.4-alpha.22"
|
||||
},
|
||||
"files": [
|
||||
|
||||
@@ -230,7 +230,7 @@ const ConfigWarning = {
|
||||
} as const;
|
||||
|
||||
type ConfigWarningStrings = keyof typeof ConfigWarning;
|
||||
const issuedWarnings: { [key in ConfigWarningStrings]?: boolean } = {};
|
||||
const issuedWarnings: Partial<Record<ConfigWarningStrings, boolean>> = {};
|
||||
const issueWarning = (warning: ConfigWarningStrings) => {
|
||||
if (issuedWarnings[warning]) {
|
||||
return;
|
||||
|
||||
@@ -795,6 +795,8 @@ export interface ErDiagramConfig extends BaseDiagramConfig {
|
||||
*
|
||||
*/
|
||||
entityPadding?: number;
|
||||
nodeSpacing?: number;
|
||||
rankSpacing?: number;
|
||||
/**
|
||||
* Stroke color of box edges and lines.
|
||||
*/
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
setDiagramTitle,
|
||||
} from '../common/commonDb.js';
|
||||
import type {
|
||||
ArchitectureAlignment,
|
||||
ArchitectureDB,
|
||||
ArchitectureDirectionPair,
|
||||
ArchitectureDirectionPairMap,
|
||||
@@ -25,6 +26,7 @@ import type {
|
||||
ArchitectureState,
|
||||
} from './architectureTypes.js';
|
||||
import {
|
||||
getArchitectureDirectionAlignment,
|
||||
getArchitectureDirectionPair,
|
||||
isArchitectureDirection,
|
||||
isArchitectureJunction,
|
||||
@@ -211,12 +213,18 @@ const addEdge = function ({
|
||||
const getEdges = (): ArchitectureEdge[] => state.records.edges;
|
||||
|
||||
/**
|
||||
* Returns the current diagram's adjacency list & spatial map.
|
||||
* Returns the current diagram's adjacency list, spatial map, & group alignments.
|
||||
* If they have not been created, run the algorithms to generate them.
|
||||
* @returns
|
||||
*/
|
||||
const getDataStructures = () => {
|
||||
if (state.records.dataStructures === undefined) {
|
||||
// Tracks how groups are aligned with one another. Generated while creating the adj list
|
||||
const groupAlignments: Record<
|
||||
string,
|
||||
Record<string, Exclude<ArchitectureAlignment, 'bend'>>
|
||||
> = {};
|
||||
|
||||
// Create an adjacency list of the diagram to perform BFS on
|
||||
// Outer reduce applied on all services
|
||||
// Inner reduce applied on the edges for a service
|
||||
@@ -224,6 +232,19 @@ const getDataStructures = () => {
|
||||
Record<string, ArchitectureDirectionPairMap>
|
||||
>((prevOuter, [id, service]) => {
|
||||
prevOuter[id] = service.edges.reduce<ArchitectureDirectionPairMap>((prevInner, edge) => {
|
||||
// track the direction groups connect to one another
|
||||
const lhsGroupId = getNode(edge.lhsId)?.in;
|
||||
const rhsGroupId = getNode(edge.rhsId)?.in;
|
||||
if (lhsGroupId && rhsGroupId && lhsGroupId !== rhsGroupId) {
|
||||
const alignment = getArchitectureDirectionAlignment(edge.lhsDir, edge.rhsDir);
|
||||
if (alignment !== 'bend') {
|
||||
groupAlignments[lhsGroupId] ??= {};
|
||||
groupAlignments[lhsGroupId][rhsGroupId] = alignment;
|
||||
groupAlignments[rhsGroupId] ??= {};
|
||||
groupAlignments[rhsGroupId][lhsGroupId] = alignment;
|
||||
}
|
||||
}
|
||||
|
||||
if (edge.lhsId === id) {
|
||||
// source is LHS
|
||||
const pair = getArchitectureDirectionPair(edge.lhsDir, edge.rhsDir);
|
||||
@@ -245,6 +266,7 @@ const getDataStructures = () => {
|
||||
// Configuration for the initial pass of BFS
|
||||
const firstId = Object.keys(adjList)[0];
|
||||
const visited = { [firstId]: 1 };
|
||||
// If a key is present in this object, it has not been visited
|
||||
const notVisited = Object.keys(adjList).reduce(
|
||||
(prev, id) => (id === firstId ? prev : { ...prev, [id]: 1 }),
|
||||
{} as Record<string, number>
|
||||
@@ -283,6 +305,7 @@ const getDataStructures = () => {
|
||||
state.records.dataStructures = {
|
||||
adjList,
|
||||
spatialMaps,
|
||||
groupAlignments,
|
||||
};
|
||||
}
|
||||
return state.records.dataStructures;
|
||||
|
||||
@@ -12,7 +12,9 @@ import { setupGraphViewbox } from '../../setupGraphViewbox.js';
|
||||
import { getConfigField } from './architectureDb.js';
|
||||
import { architectureIcons } from './architectureIcons.js';
|
||||
import type {
|
||||
ArchitectureAlignment,
|
||||
ArchitectureDataStructures,
|
||||
ArchitectureGroupAlignments,
|
||||
ArchitectureJunction,
|
||||
ArchitectureSpatialMap,
|
||||
EdgeSingular,
|
||||
@@ -149,25 +151,91 @@ function addEdges(edges: ArchitectureEdge[], cy: cytoscape.Core) {
|
||||
});
|
||||
}
|
||||
|
||||
function getAlignments(spatialMaps: ArchitectureSpatialMap[]): fcose.FcoseAlignmentConstraint {
|
||||
function getAlignments(
|
||||
db: ArchitectureDB,
|
||||
spatialMaps: ArchitectureSpatialMap[],
|
||||
groupAlignments: ArchitectureGroupAlignments
|
||||
): fcose.FcoseAlignmentConstraint {
|
||||
/**
|
||||
* Flattens the alignment object so nodes in different groups will be in the same alignment array IFF their groups don't connect in a conflicting alignment
|
||||
*
|
||||
* i.e., two groups which connect horizontally should not have nodes with vertical alignments to one another
|
||||
*
|
||||
* See: #5952
|
||||
*
|
||||
* @param alignmentObj - alignment object with the outer key being the row/col # and the inner key being the group name mapped to the nodes on that axis in the group
|
||||
* @param alignmentDir - alignment direction
|
||||
* @returns flattened alignment object with an arbitrary key mapping to nodes in the same row/col
|
||||
*/
|
||||
const flattenAlignments = (
|
||||
alignmentObj: Record<number, Record<string, string[]>>,
|
||||
alignmentDir: ArchitectureAlignment
|
||||
): Record<string, string[]> => {
|
||||
return Object.entries(alignmentObj).reduce(
|
||||
(prev, [dir, alignments]) => {
|
||||
// prev is the mapping of x/y coordinate to an array of the nodes in that row/column
|
||||
let cnt = 0;
|
||||
const arr = Object.entries(alignments); // [group name, array of nodes within the group on axis dir]
|
||||
if (arr.length === 1) {
|
||||
// If only one group exists in the row/column, we don't need to do anything else
|
||||
prev[dir] = arr[0][1];
|
||||
return prev;
|
||||
}
|
||||
for (let i = 0; i < arr.length - 1; i++) {
|
||||
for (let j = i + 1; j < arr.length; j++) {
|
||||
const [aGroupId, aNodeIds] = arr[i];
|
||||
const [bGroupId, bNodeIds] = arr[j];
|
||||
const alignment = groupAlignments[aGroupId]?.[bGroupId]; // Get how the two groups are intended to align (undefined if they aren't)
|
||||
|
||||
if (alignment === alignmentDir) {
|
||||
// If the intended alignment between the two groups is the same as the alignment we are parsing
|
||||
prev[dir] ??= [];
|
||||
prev[dir] = [...prev[dir], ...aNodeIds, ...bNodeIds]; // add the node ids of both groups to the axis array in prev
|
||||
} else if (aGroupId === 'default' || bGroupId === 'default') {
|
||||
// If either of the groups are in the default space (not in a group), use the same behavior as above
|
||||
prev[dir] ??= [];
|
||||
prev[dir] = [...prev[dir], ...aNodeIds, ...bNodeIds];
|
||||
} else {
|
||||
// Otherwise, the nodes in the two groups are not intended to align
|
||||
const keyA = `${dir}-${cnt++}`;
|
||||
prev[keyA] = aNodeIds;
|
||||
const keyB = `${dir}-${cnt++}`;
|
||||
prev[keyB] = bNodeIds;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return prev;
|
||||
},
|
||||
{} as Record<string, string[]>
|
||||
);
|
||||
};
|
||||
|
||||
const alignments = spatialMaps.map((spatialMap) => {
|
||||
const horizontalAlignments: Record<number, string[]> = {};
|
||||
const verticalAlignments: Record<number, string[]> = {};
|
||||
const horizontalAlignments: Record<number, Record<string, string[]>> = {};
|
||||
const verticalAlignments: Record<number, Record<string, string[]>> = {};
|
||||
|
||||
// Group service ids in an object with their x and y coordinate as the key
|
||||
Object.entries(spatialMap).forEach(([id, [x, y]]) => {
|
||||
if (!horizontalAlignments[y]) {
|
||||
horizontalAlignments[y] = [];
|
||||
}
|
||||
if (!verticalAlignments[x]) {
|
||||
verticalAlignments[x] = [];
|
||||
}
|
||||
horizontalAlignments[y].push(id);
|
||||
verticalAlignments[x].push(id);
|
||||
const nodeGroup = db.getNode(id)?.in ?? 'default';
|
||||
|
||||
horizontalAlignments[y] ??= {};
|
||||
horizontalAlignments[y][nodeGroup] ??= [];
|
||||
horizontalAlignments[y][nodeGroup].push(id);
|
||||
|
||||
verticalAlignments[x] ??= {};
|
||||
verticalAlignments[x][nodeGroup] ??= [];
|
||||
verticalAlignments[x][nodeGroup].push(id);
|
||||
});
|
||||
|
||||
// Merge the values of each object into a list if the inner list has at least 2 elements
|
||||
return {
|
||||
horiz: Object.values(horizontalAlignments).filter((arr) => arr.length > 1),
|
||||
vert: Object.values(verticalAlignments).filter((arr) => arr.length > 1),
|
||||
horiz: Object.values(flattenAlignments(horizontalAlignments, 'horizontal')).filter(
|
||||
(arr) => arr.length > 1
|
||||
),
|
||||
vert: Object.values(flattenAlignments(verticalAlignments, 'vertical')).filter(
|
||||
(arr) => arr.length > 1
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -244,7 +312,8 @@ function layoutArchitecture(
|
||||
junctions: ArchitectureJunction[],
|
||||
groups: ArchitectureGroup[],
|
||||
edges: ArchitectureEdge[],
|
||||
{ spatialMaps }: ArchitectureDataStructures
|
||||
db: ArchitectureDB,
|
||||
{ spatialMaps, groupAlignments }: ArchitectureDataStructures
|
||||
): Promise<cytoscape.Core> {
|
||||
return new Promise((resolve) => {
|
||||
const renderEl = select('body').append('div').attr('id', 'cy').attr('style', 'display:none');
|
||||
@@ -318,9 +387,8 @@ function layoutArchitecture(
|
||||
addServices(services, cy);
|
||||
addJunctions(junctions, cy);
|
||||
addEdges(edges, cy);
|
||||
|
||||
// Use the spatial map to create alignment arrays for fcose
|
||||
const alignmentConstraint = getAlignments(spatialMaps);
|
||||
const alignmentConstraint = getAlignments(db, spatialMaps, groupAlignments);
|
||||
|
||||
// Create the relative constraints for fcose by using an inverse of the spatial map and performing BFS on it
|
||||
const relativePlacementConstraint = getRelativeConstraints(spatialMaps);
|
||||
@@ -454,7 +522,7 @@ export const draw: DrawDefinition = async (text, id, _version, diagObj: Diagram)
|
||||
await drawServices(db, servicesElem, services);
|
||||
drawJunctions(db, servicesElem, junctions);
|
||||
|
||||
const cy = await layoutArchitecture(services, junctions, groups, edges, ds);
|
||||
const cy = await layoutArchitecture(services, junctions, groups, edges, db, ds);
|
||||
|
||||
await drawEdges(edgesElem, cy);
|
||||
await drawGroups(groupElem, cy);
|
||||
|
||||
@@ -7,6 +7,8 @@ import type cytoscape from 'cytoscape';
|
||||
| Architecture Diagram Types |
|
||||
\*=======================================*/
|
||||
|
||||
export type ArchitectureAlignment = 'vertical' | 'horizontal' | 'bend';
|
||||
|
||||
export type ArchitectureDirection = 'L' | 'R' | 'T' | 'B';
|
||||
export type ArchitectureDirectionX = Extract<ArchitectureDirection, 'L' | 'R'>;
|
||||
export type ArchitectureDirectionY = Extract<ArchitectureDirection, 'T' | 'B'>;
|
||||
@@ -104,9 +106,7 @@ export const isValidArchitectureDirectionPair = function (
|
||||
return x !== 'LL' && x !== 'RR' && x !== 'TT' && x !== 'BB';
|
||||
};
|
||||
|
||||
export type ArchitectureDirectionPairMap = {
|
||||
[key in ArchitectureDirectionPair]?: string;
|
||||
};
|
||||
export type ArchitectureDirectionPairMap = Partial<Record<ArchitectureDirectionPair, string>>;
|
||||
|
||||
/**
|
||||
* Creates a pair of the directions of each side of an edge. This function should be used instead of manually creating it to ensure that the source is always the first character.
|
||||
@@ -170,6 +170,18 @@ export const getArchitectureDirectionXYFactors = function (
|
||||
}
|
||||
};
|
||||
|
||||
export const getArchitectureDirectionAlignment = function (
|
||||
a: ArchitectureDirection,
|
||||
b: ArchitectureDirection
|
||||
): ArchitectureAlignment {
|
||||
if (isArchitectureDirectionXY(a, b)) {
|
||||
return 'bend';
|
||||
} else if (isArchitectureDirectionX(a)) {
|
||||
return 'horizontal';
|
||||
}
|
||||
return 'vertical';
|
||||
};
|
||||
|
||||
export interface ArchitectureStyleOptions {
|
||||
archEdgeColor: string;
|
||||
archEdgeArrowColor: string;
|
||||
@@ -249,9 +261,27 @@ export interface ArchitectureDB extends DiagramDB {
|
||||
|
||||
export type ArchitectureAdjacencyList = Record<string, ArchitectureDirectionPairMap>;
|
||||
export type ArchitectureSpatialMap = Record<string, number[]>;
|
||||
|
||||
/**
|
||||
* Maps the direction that groups connect from.
|
||||
*
|
||||
* **Outer key**: ID of group A
|
||||
*
|
||||
* **Inner key**: ID of group B
|
||||
*
|
||||
* **Value**: 'vertical' or 'horizontal'
|
||||
*
|
||||
* Note: tmp[groupA][groupB] == tmp[groupB][groupA]
|
||||
*/
|
||||
export type ArchitectureGroupAlignments = Record<
|
||||
string,
|
||||
Record<string, Exclude<ArchitectureAlignment, 'bend'>>
|
||||
>;
|
||||
|
||||
export interface ArchitectureDataStructures {
|
||||
adjList: ArchitectureAdjacencyList;
|
||||
spatialMaps: ArchitectureSpatialMap[];
|
||||
groupAlignments: ArchitectureGroupAlignments;
|
||||
}
|
||||
|
||||
export interface ArchitectureState extends Record<string, unknown> {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,11 @@
|
||||
import { parser } from './parser/classDiagram.jison';
|
||||
import classDb from './classDb.js';
|
||||
import { ClassDB } from './classDb.js';
|
||||
|
||||
describe('class diagram, ', function () {
|
||||
describe('when parsing data from a classDiagram it', function () {
|
||||
let classDb;
|
||||
beforeEach(function () {
|
||||
classDb = new ClassDB();
|
||||
parser.yy = classDb;
|
||||
parser.yy.clear();
|
||||
});
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import type { DiagramDefinition } from '../../diagram-api/types.js';
|
||||
// @ts-ignore: JISON doesn't support types
|
||||
import parser from './parser/classDiagram.jison';
|
||||
import db from './classDb.js';
|
||||
import { ClassDB } from './classDb.js';
|
||||
import styles from './styles.js';
|
||||
import renderer from './classRenderer-v3-unified.js';
|
||||
|
||||
export const diagram: DiagramDefinition = {
|
||||
parser,
|
||||
db,
|
||||
get db() {
|
||||
return new ClassDB();
|
||||
},
|
||||
renderer,
|
||||
styles,
|
||||
init: (cnf) => {
|
||||
@@ -15,6 +17,5 @@ export const diagram: DiagramDefinition = {
|
||||
cnf.class = {};
|
||||
}
|
||||
cnf.class.arrowMarkerAbsolute = cnf.arrowMarkerAbsolute;
|
||||
db.clear();
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/* eslint-disable @typescript-eslint/unbound-method -- Broken for Vitest mocks, see https://github.com/vitest-dev/eslint-plugin-vitest/pull/286 */
|
||||
// @ts-expect-error Jison doesn't export types
|
||||
import { parser } from './parser/classDiagram.jison';
|
||||
import classDb from './classDb.js';
|
||||
import { ClassDB } from './classDb.js';
|
||||
import { vi, describe, it, expect } from 'vitest';
|
||||
import type { ClassMap, NamespaceNode } from './classTypes.js';
|
||||
const spyOn = vi.spyOn;
|
||||
@@ -10,8 +11,9 @@ const abstractCssStyle = 'font-style:italic;';
|
||||
|
||||
describe('given a basic class diagram, ', function () {
|
||||
describe('when parsing class definition', function () {
|
||||
let classDb: ClassDB;
|
||||
beforeEach(function () {
|
||||
classDb.clear();
|
||||
classDb = new ClassDB();
|
||||
parser.yy = classDb;
|
||||
});
|
||||
it('should handle classes within namespaces', () => {
|
||||
@@ -564,8 +566,9 @@ class C13["With Città foreign language"]
|
||||
});
|
||||
|
||||
describe('when parsing class defined in brackets', function () {
|
||||
let classDb: ClassDB;
|
||||
beforeEach(function () {
|
||||
classDb.clear();
|
||||
classDb = new ClassDB();
|
||||
parser.yy = classDb;
|
||||
});
|
||||
|
||||
@@ -656,8 +659,9 @@ class C13["With Città foreign language"]
|
||||
});
|
||||
|
||||
describe('when parsing comments', function () {
|
||||
let classDb: ClassDB;
|
||||
beforeEach(function () {
|
||||
classDb.clear();
|
||||
classDb = new ClassDB();
|
||||
parser.yy = classDb;
|
||||
});
|
||||
|
||||
@@ -746,8 +750,9 @@ foo()
|
||||
});
|
||||
|
||||
describe('when parsing click statements', function () {
|
||||
let classDb: ClassDB;
|
||||
beforeEach(function () {
|
||||
classDb.clear();
|
||||
classDb = new ClassDB();
|
||||
parser.yy = classDb;
|
||||
});
|
||||
it('should handle href link', function () {
|
||||
@@ -857,8 +862,9 @@ foo()
|
||||
});
|
||||
|
||||
describe('when parsing annotations', function () {
|
||||
let classDb: ClassDB;
|
||||
beforeEach(function () {
|
||||
classDb.clear();
|
||||
classDb = new ClassDB();
|
||||
parser.yy = classDb;
|
||||
});
|
||||
|
||||
@@ -921,8 +927,9 @@ foo()
|
||||
|
||||
describe('given a class diagram with members and methods ', function () {
|
||||
describe('when parsing members', function () {
|
||||
let classDb: ClassDB;
|
||||
beforeEach(function () {
|
||||
classDb.clear();
|
||||
classDb = new ClassDB();
|
||||
parser.yy = classDb;
|
||||
});
|
||||
|
||||
@@ -980,8 +987,9 @@ describe('given a class diagram with members and methods ', function () {
|
||||
});
|
||||
|
||||
describe('when parsing method definition', function () {
|
||||
let classDb: ClassDB;
|
||||
beforeEach(function () {
|
||||
classDb.clear();
|
||||
classDb = new ClassDB();
|
||||
parser.yy = classDb;
|
||||
});
|
||||
|
||||
@@ -1067,8 +1075,9 @@ describe('given a class diagram with members and methods ', function () {
|
||||
|
||||
describe('given a class diagram with generics, ', function () {
|
||||
describe('when parsing valid generic classes', function () {
|
||||
let classDb: ClassDB;
|
||||
beforeEach(function () {
|
||||
classDb.clear();
|
||||
classDb = new ClassDB();
|
||||
parser.yy = classDb;
|
||||
});
|
||||
|
||||
@@ -1180,8 +1189,9 @@ namespace space {
|
||||
|
||||
describe('given a class diagram with relationships, ', function () {
|
||||
describe('when parsing basic relationships', function () {
|
||||
let classDb: ClassDB;
|
||||
beforeEach(function () {
|
||||
classDb.clear();
|
||||
classDb = new ClassDB();
|
||||
parser.yy = classDb;
|
||||
});
|
||||
|
||||
@@ -1714,7 +1724,9 @@ class Class2
|
||||
});
|
||||
|
||||
describe('when parsing classDiagram with text labels', () => {
|
||||
let classDb: ClassDB;
|
||||
beforeEach(function () {
|
||||
classDb = new ClassDB();
|
||||
parser.yy = classDb;
|
||||
parser.yy.clear();
|
||||
});
|
||||
@@ -1897,3 +1909,40 @@ class C13["With Città foreign language"]
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('class db class', () => {
|
||||
let classDb: ClassDB;
|
||||
beforeEach(() => {
|
||||
classDb = new ClassDB();
|
||||
});
|
||||
// This is to ensure that functions used in class JISON are exposed as function from ClassDB
|
||||
it('should have functions used in class JISON as own property', () => {
|
||||
const functionsUsedInParser = [
|
||||
'addRelation',
|
||||
'cleanupLabel',
|
||||
'setAccTitle',
|
||||
'setAccDescription',
|
||||
'addClassesToNamespace',
|
||||
'addNamespace',
|
||||
'setCssClass',
|
||||
'addMembers',
|
||||
'addClass',
|
||||
'setClassLabel',
|
||||
'addAnnotation',
|
||||
'addMember',
|
||||
'addNote',
|
||||
'defineClass',
|
||||
'setDirection',
|
||||
'relationType',
|
||||
'lineType',
|
||||
'setClickEvent',
|
||||
'setTooltip',
|
||||
'setLink',
|
||||
'setCssStyle',
|
||||
] as const satisfies (keyof ClassDB)[];
|
||||
|
||||
for (const fun of functionsUsedInParser) {
|
||||
expect(Object.hasOwn(classDb, fun)).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import type { DiagramDefinition } from '../../diagram-api/types.js';
|
||||
// @ts-ignore: JISON doesn't support types
|
||||
import parser from './parser/classDiagram.jison';
|
||||
import db from './classDb.js';
|
||||
import { ClassDB } from './classDb.js';
|
||||
import styles from './styles.js';
|
||||
import renderer from './classRenderer-v3-unified.js';
|
||||
|
||||
export const diagram: DiagramDefinition = {
|
||||
parser,
|
||||
db,
|
||||
get db() {
|
||||
return new ClassDB();
|
||||
},
|
||||
renderer,
|
||||
styles,
|
||||
init: (cnf) => {
|
||||
@@ -15,6 +17,5 @@ export const diagram: DiagramDefinition = {
|
||||
cnf.class = {};
|
||||
}
|
||||
cnf.class.arrowMarkerAbsolute = cnf.arrowMarkerAbsolute;
|
||||
db.clear();
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { parser } from './classDiagram.jison';
|
||||
import classDb from '../classDb.js';
|
||||
import { ClassDB } from '../classDb.js';
|
||||
|
||||
describe('class diagram', function () {
|
||||
let classDb;
|
||||
beforeEach(function () {
|
||||
classDb = new ClassDB();
|
||||
parser.yy = classDb;
|
||||
parser.yy.clear();
|
||||
});
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
import { log } from '../../logger.js';
|
||||
import { getConfig } from '../../diagram-api/diagramAPI.js';
|
||||
|
||||
import {
|
||||
setAccTitle,
|
||||
getAccTitle,
|
||||
getAccDescription,
|
||||
setAccDescription,
|
||||
clear as commonClear,
|
||||
setDiagramTitle,
|
||||
getDiagramTitle,
|
||||
} from '../common/commonDb.js';
|
||||
|
||||
let entities = new Map();
|
||||
let relationships = [];
|
||||
|
||||
const Cardinality = {
|
||||
ZERO_OR_ONE: 'ZERO_OR_ONE',
|
||||
ZERO_OR_MORE: 'ZERO_OR_MORE',
|
||||
ONE_OR_MORE: 'ONE_OR_MORE',
|
||||
ONLY_ONE: 'ONLY_ONE',
|
||||
MD_PARENT: 'MD_PARENT',
|
||||
};
|
||||
|
||||
const Identification = {
|
||||
NON_IDENTIFYING: 'NON_IDENTIFYING',
|
||||
IDENTIFYING: 'IDENTIFYING',
|
||||
};
|
||||
/**
|
||||
* Add entity
|
||||
* @param {string} name - The name of the entity
|
||||
* @param {string | undefined} alias - The alias of the entity
|
||||
*/
|
||||
const addEntity = function (name, alias = undefined) {
|
||||
if (!entities.has(name)) {
|
||||
entities.set(name, { attributes: [], alias });
|
||||
log.info('Added new entity :', name);
|
||||
} else if (!entities.get(name).alias && alias) {
|
||||
entities.get(name).alias = alias;
|
||||
log.info(`Add alias '${alias}' to entity '${name}'`);
|
||||
}
|
||||
|
||||
return entities.get(name);
|
||||
};
|
||||
|
||||
const getEntities = () => entities;
|
||||
|
||||
const addAttributes = function (entityName, attribs) {
|
||||
let entity = addEntity(entityName); // May do nothing (if entity has already been added)
|
||||
|
||||
// Process attribs in reverse order due to effect of recursive construction (last attribute is first)
|
||||
let i;
|
||||
for (i = attribs.length - 1; i >= 0; i--) {
|
||||
entity.attributes.push(attribs[i]);
|
||||
log.debug('Added attribute ', attribs[i].attributeName);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Add a relationship
|
||||
*
|
||||
* @param entA The first entity in the relationship
|
||||
* @param rolA The role played by the first entity in relation to the second
|
||||
* @param entB The second entity in the relationship
|
||||
* @param rSpec The details of the relationship between the two entities
|
||||
*/
|
||||
const addRelationship = function (entA, rolA, entB, rSpec) {
|
||||
let rel = {
|
||||
entityA: entA,
|
||||
roleA: rolA,
|
||||
entityB: entB,
|
||||
relSpec: rSpec,
|
||||
};
|
||||
|
||||
relationships.push(rel);
|
||||
log.debug('Added new relationship :', rel);
|
||||
};
|
||||
|
||||
const getRelationships = () => relationships;
|
||||
|
||||
const clear = function () {
|
||||
entities = new Map();
|
||||
relationships = [];
|
||||
commonClear();
|
||||
};
|
||||
|
||||
export default {
|
||||
Cardinality,
|
||||
Identification,
|
||||
getConfig: () => getConfig().er,
|
||||
addEntity,
|
||||
addAttributes,
|
||||
getEntities,
|
||||
addRelationship,
|
||||
getRelationships,
|
||||
clear,
|
||||
setAccTitle,
|
||||
getAccTitle,
|
||||
setAccDescription,
|
||||
getAccDescription,
|
||||
setDiagramTitle,
|
||||
getDiagramTitle,
|
||||
};
|
||||
251
packages/mermaid/src/diagrams/er/erDb.ts
Normal file
251
packages/mermaid/src/diagrams/er/erDb.ts
Normal file
@@ -0,0 +1,251 @@
|
||||
import { log } from '../../logger.js';
|
||||
import { getConfig } from '../../diagram-api/diagramAPI.js';
|
||||
import type { Edge, Node } from '../../rendering-util/types.js';
|
||||
import type { EntityNode, Attribute, Relationship, EntityClass, RelSpec } from './erTypes.js';
|
||||
import {
|
||||
setAccTitle,
|
||||
getAccTitle,
|
||||
getAccDescription,
|
||||
setAccDescription,
|
||||
clear as commonClear,
|
||||
setDiagramTitle,
|
||||
getDiagramTitle,
|
||||
} from '../common/commonDb.js';
|
||||
import { getEdgeId } from '../../utils.js';
|
||||
import type { DiagramDB } from '../../diagram-api/types.js';
|
||||
|
||||
export class ErDB implements DiagramDB {
|
||||
private entities = new Map<string, EntityNode>();
|
||||
private relationships: Relationship[] = [];
|
||||
private classes = new Map<string, EntityClass>();
|
||||
private direction = 'TB';
|
||||
|
||||
private Cardinality = {
|
||||
ZERO_OR_ONE: 'ZERO_OR_ONE',
|
||||
ZERO_OR_MORE: 'ZERO_OR_MORE',
|
||||
ONE_OR_MORE: 'ONE_OR_MORE',
|
||||
ONLY_ONE: 'ONLY_ONE',
|
||||
MD_PARENT: 'MD_PARENT',
|
||||
};
|
||||
|
||||
private Identification = {
|
||||
NON_IDENTIFYING: 'NON_IDENTIFYING',
|
||||
IDENTIFYING: 'IDENTIFYING',
|
||||
};
|
||||
|
||||
constructor() {
|
||||
this.clear();
|
||||
this.addEntity = this.addEntity.bind(this);
|
||||
this.addAttributes = this.addAttributes.bind(this);
|
||||
this.addRelationship = this.addRelationship.bind(this);
|
||||
this.setDirection = this.setDirection.bind(this);
|
||||
this.addCssStyles = this.addCssStyles.bind(this);
|
||||
this.addClass = this.addClass.bind(this);
|
||||
this.setClass = this.setClass.bind(this);
|
||||
this.setAccTitle = this.setAccTitle.bind(this);
|
||||
this.setAccDescription = this.setAccDescription.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add entity
|
||||
* @param name - The name of the entity
|
||||
* @param alias - The alias of the entity
|
||||
*/
|
||||
public addEntity(name: string, alias = ''): EntityNode {
|
||||
if (!this.entities.has(name)) {
|
||||
this.entities.set(name, {
|
||||
id: `entity-${name}-${this.entities.size}`,
|
||||
label: name,
|
||||
attributes: [],
|
||||
alias,
|
||||
shape: 'erBox',
|
||||
look: getConfig().look ?? 'default',
|
||||
cssClasses: 'default',
|
||||
cssStyles: [],
|
||||
});
|
||||
log.info('Added new entity :', name);
|
||||
} else if (!this.entities.get(name)?.alias && alias) {
|
||||
this.entities.get(name)!.alias = alias;
|
||||
log.info(`Add alias '${alias}' to entity '${name}'`);
|
||||
}
|
||||
|
||||
return this.entities.get(name)!;
|
||||
}
|
||||
|
||||
public getEntity(name: string) {
|
||||
return this.entities.get(name);
|
||||
}
|
||||
|
||||
public getEntities() {
|
||||
return this.entities;
|
||||
}
|
||||
|
||||
public getClasses() {
|
||||
return this.classes;
|
||||
}
|
||||
|
||||
public addAttributes(entityName: string, attribs: Attribute[]) {
|
||||
const entity = this.addEntity(entityName); // May do nothing (if entity has already been added)
|
||||
|
||||
// Process attribs in reverse order due to effect of recursive construction (last attribute is first)
|
||||
let i;
|
||||
for (i = attribs.length - 1; i >= 0; i--) {
|
||||
if (!attribs[i].keys) {
|
||||
attribs[i].keys = [];
|
||||
}
|
||||
if (!attribs[i].comment) {
|
||||
attribs[i].comment = '';
|
||||
}
|
||||
entity.attributes.push(attribs[i]);
|
||||
log.debug('Added attribute ', attribs[i].name);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a relationship
|
||||
*
|
||||
* @param entA - The first entity in the relationship
|
||||
* @param rolA - The role played by the first entity in relation to the second
|
||||
* @param entB - The second entity in the relationship
|
||||
* @param rSpec - The details of the relationship between the two entities
|
||||
*/
|
||||
public addRelationship(entA: string, rolA: string, entB: string, rSpec: RelSpec) {
|
||||
const entityA = this.entities.get(entA);
|
||||
const entityB = this.entities.get(entB);
|
||||
if (!entityA || !entityB) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rel = {
|
||||
entityA: entityA.id,
|
||||
roleA: rolA,
|
||||
entityB: entityB.id,
|
||||
relSpec: rSpec,
|
||||
};
|
||||
|
||||
this.relationships.push(rel);
|
||||
log.debug('Added new relationship :', rel);
|
||||
}
|
||||
|
||||
public getRelationships() {
|
||||
return this.relationships;
|
||||
}
|
||||
|
||||
public getDirection() {
|
||||
return this.direction;
|
||||
}
|
||||
|
||||
public setDirection(dir: string) {
|
||||
this.direction = dir;
|
||||
}
|
||||
|
||||
private getCompiledStyles(classDefs: string[]) {
|
||||
let compiledStyles: string[] = [];
|
||||
for (const customClass of classDefs) {
|
||||
const cssClass = this.classes.get(customClass);
|
||||
if (cssClass?.styles) {
|
||||
compiledStyles = [...compiledStyles, ...(cssClass.styles ?? [])].map((s) => s.trim());
|
||||
}
|
||||
if (cssClass?.textStyles) {
|
||||
compiledStyles = [...compiledStyles, ...(cssClass.textStyles ?? [])].map((s) => s.trim());
|
||||
}
|
||||
}
|
||||
return compiledStyles;
|
||||
}
|
||||
|
||||
public addCssStyles(ids: string[], styles: string[]) {
|
||||
for (const id of ids) {
|
||||
const entity = this.entities.get(id);
|
||||
if (!styles || !entity) {
|
||||
return;
|
||||
}
|
||||
for (const style of styles) {
|
||||
entity.cssStyles!.push(style);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public addClass(ids: string[], style: string[]) {
|
||||
ids.forEach((id) => {
|
||||
let classNode = this.classes.get(id);
|
||||
if (classNode === undefined) {
|
||||
classNode = { id, styles: [], textStyles: [] };
|
||||
this.classes.set(id, classNode);
|
||||
}
|
||||
|
||||
if (style) {
|
||||
style.forEach(function (s) {
|
||||
if (/color/.exec(s)) {
|
||||
const newStyle = s.replace('fill', 'bgFill');
|
||||
classNode.textStyles.push(newStyle);
|
||||
}
|
||||
classNode.styles.push(s);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public setClass(ids: string[], classNames: string[]) {
|
||||
for (const id of ids) {
|
||||
const entity = this.entities.get(id);
|
||||
if (entity) {
|
||||
for (const className of classNames) {
|
||||
entity.cssClasses += ' ' + className;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public clear() {
|
||||
this.entities = new Map();
|
||||
this.classes = new Map();
|
||||
this.relationships = [];
|
||||
commonClear();
|
||||
}
|
||||
|
||||
public getData() {
|
||||
const nodes: Node[] = [];
|
||||
const edges: Edge[] = [];
|
||||
const config = getConfig();
|
||||
|
||||
for (const entityKey of this.entities.keys()) {
|
||||
const entityNode = this.entities.get(entityKey);
|
||||
if (entityNode) {
|
||||
entityNode.cssCompiledStyles = this.getCompiledStyles(entityNode.cssClasses!.split(' '));
|
||||
nodes.push(entityNode as unknown as Node);
|
||||
}
|
||||
}
|
||||
|
||||
let count = 0;
|
||||
for (const relationship of this.relationships) {
|
||||
const edge: Edge = {
|
||||
id: getEdgeId(relationship.entityA, relationship.entityB, {
|
||||
prefix: 'id',
|
||||
counter: count++,
|
||||
}),
|
||||
type: 'normal',
|
||||
curve: 'basis',
|
||||
start: relationship.entityA,
|
||||
end: relationship.entityB,
|
||||
label: relationship.roleA,
|
||||
labelpos: 'c',
|
||||
thickness: 'normal',
|
||||
classes: 'relationshipLine',
|
||||
arrowTypeStart: relationship.relSpec.cardB.toLowerCase(),
|
||||
arrowTypeEnd: relationship.relSpec.cardA.toLowerCase(),
|
||||
pattern: relationship.relSpec.relType == 'IDENTIFYING' ? 'solid' : 'dashed',
|
||||
look: config.look,
|
||||
};
|
||||
edges.push(edge);
|
||||
}
|
||||
return { nodes, edges, other: {}, config, direction: 'TB' };
|
||||
}
|
||||
|
||||
public setAccTitle = setAccTitle;
|
||||
public getAccTitle = getAccTitle;
|
||||
public setAccDescription = setAccDescription;
|
||||
public getAccDescription = getAccDescription;
|
||||
public setDiagramTitle = setDiagramTitle;
|
||||
public getDiagramTitle = getDiagramTitle;
|
||||
public getConfig = () => getConfig().er;
|
||||
}
|
||||
@@ -1,12 +1,14 @@
|
||||
// @ts-ignore: TODO: Fix ts errors
|
||||
import erParser from './parser/erDiagram.jison';
|
||||
import erDb from './erDb.js';
|
||||
import erRenderer from './erRenderer.js';
|
||||
import { ErDB } from './erDb.js';
|
||||
import * as renderer from './erRenderer-unified.js';
|
||||
import erStyles from './styles.js';
|
||||
|
||||
export const diagram = {
|
||||
parser: erParser,
|
||||
db: erDb,
|
||||
renderer: erRenderer,
|
||||
get db() {
|
||||
return new ErDB();
|
||||
},
|
||||
renderer,
|
||||
styles: erStyles,
|
||||
};
|
||||
|
||||
66
packages/mermaid/src/diagrams/er/erRenderer-unified.ts
Normal file
66
packages/mermaid/src/diagrams/er/erRenderer-unified.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { getConfig } from '../../diagram-api/diagramAPI.js';
|
||||
import { log } from '../../logger.js';
|
||||
import { getDiagramElement } from '../../rendering-util/insertElementsForSize.js';
|
||||
import { getRegisteredLayoutAlgorithm, render } from '../../rendering-util/render.js';
|
||||
import { setupViewPortForSVG } from '../../rendering-util/setupViewPortForSVG.js';
|
||||
import type { LayoutData } from '../../rendering-util/types.js';
|
||||
import utils from '../../utils.js';
|
||||
import { select } from 'd3';
|
||||
|
||||
export const draw = async function (text: string, id: string, _version: string, diag: any) {
|
||||
log.info('REF0:');
|
||||
log.info('Drawing er diagram (unified)', id);
|
||||
const { securityLevel, er: conf, layout } = getConfig();
|
||||
|
||||
// The getData method provided in all supported diagrams is used to extract the data from the parsed structure
|
||||
// into the Layout data format
|
||||
const data4Layout = diag.db.getData() as LayoutData;
|
||||
|
||||
// Create the root SVG - the element is the div containing the SVG element
|
||||
const svg = getDiagramElement(id, securityLevel);
|
||||
|
||||
data4Layout.type = diag.type;
|
||||
data4Layout.layoutAlgorithm = getRegisteredLayoutAlgorithm(layout);
|
||||
|
||||
// Workaround as when rendering and setting up the graph it uses flowchart spacing before data4Layout spacing?
|
||||
data4Layout.config.flowchart!.nodeSpacing = conf?.nodeSpacing || 140;
|
||||
data4Layout.config.flowchart!.rankSpacing = conf?.rankSpacing || 80;
|
||||
data4Layout.direction = diag.db.getDirection();
|
||||
|
||||
data4Layout.markers = ['only_one', 'zero_or_one', 'one_or_more', 'zero_or_more'];
|
||||
data4Layout.diagramId = id;
|
||||
await render(data4Layout, svg);
|
||||
// Elk layout algorithm displays markers above nodes, so move edges to top so they are "painted" over by the nodes.
|
||||
if (data4Layout.layoutAlgorithm === 'elk') {
|
||||
svg.select('.edges').lower();
|
||||
}
|
||||
|
||||
// Sets the background nodes to the same position as their original counterparts.
|
||||
// Background nodes are created when the look is handDrawn so the ER diagram markers do not show underneath.
|
||||
const backgroundNodes = svg.selectAll('[id*="-background"]');
|
||||
// eslint-disable-next-line unicorn/prefer-spread
|
||||
if (Array.from(backgroundNodes).length > 0) {
|
||||
backgroundNodes.each(function (this: SVGElement) {
|
||||
const backgroundNode = select(this);
|
||||
const backgroundId = backgroundNode.attr('id');
|
||||
|
||||
const nonBackgroundId = backgroundId.replace('-background', '');
|
||||
const nonBackgroundNode = svg.select(`#${CSS.escape(nonBackgroundId)}`);
|
||||
|
||||
if (!nonBackgroundNode.empty()) {
|
||||
const transform = nonBackgroundNode.attr('transform');
|
||||
backgroundNode.attr('transform', transform);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const padding = 8;
|
||||
utils.insertTitle(
|
||||
svg,
|
||||
'erDiagramTitleText',
|
||||
conf?.titleTopMargin ?? 25,
|
||||
diag.db.getDiagramTitle()
|
||||
);
|
||||
|
||||
setupViewPortForSVG(svg, padding, 'erDiagram', conf?.useMaxWidth ?? true);
|
||||
};
|
||||
37
packages/mermaid/src/diagrams/er/erTypes.ts
Normal file
37
packages/mermaid/src/diagrams/er/erTypes.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
export interface EntityNode {
|
||||
id: string;
|
||||
label: string;
|
||||
attributes: Attribute[];
|
||||
alias: string;
|
||||
shape: string;
|
||||
look?: string;
|
||||
cssClasses?: string;
|
||||
cssStyles?: string[];
|
||||
cssCompiledStyles?: string[];
|
||||
}
|
||||
|
||||
export interface Attribute {
|
||||
type: string;
|
||||
name: string;
|
||||
keys: ('PK' | 'FK' | 'UK')[];
|
||||
comment: string;
|
||||
}
|
||||
|
||||
export interface Relationship {
|
||||
entityA: string;
|
||||
roleA: string;
|
||||
entityB: string;
|
||||
relSpec: RelSpec;
|
||||
}
|
||||
|
||||
export interface RelSpec {
|
||||
cardA: string;
|
||||
cardB: string;
|
||||
relType: string;
|
||||
}
|
||||
|
||||
export interface EntityClass {
|
||||
id: string;
|
||||
styles: string[];
|
||||
textStyles: string[];
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
%x acc_title
|
||||
%x acc_descr
|
||||
%x acc_descr_multiline
|
||||
%x style
|
||||
|
||||
%%
|
||||
accTitle\s*":"\s* { this.begin("acc_title");return 'acc_title'; }
|
||||
@@ -14,6 +15,10 @@ accDescr\s*":"\s* { this.begin("ac
|
||||
accDescr\s*"{"\s* { this.begin("acc_descr_multiline");}
|
||||
<acc_descr_multiline>[\}] { this.popState(); }
|
||||
<acc_descr_multiline>[^\}]* return "acc_descr_multiline_value";
|
||||
.*direction\s+TB[^\n]* return 'direction_tb';
|
||||
.*direction\s+BT[^\n]* return 'direction_bt';
|
||||
.*direction\s+RL[^\n]* return 'direction_rl';
|
||||
.*direction\s+LR[^\n]* return 'direction_lr';
|
||||
[\n]+ return 'NEWLINE';
|
||||
\s+ /* skip whitespace */
|
||||
[\s]+ return 'SPACE';
|
||||
@@ -21,11 +26,15 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multili
|
||||
\"[^"]*\" return 'WORD';
|
||||
"erDiagram" return 'ER_DIAGRAM';
|
||||
"{" { this.begin("block"); return 'BLOCK_START'; }
|
||||
<block>"," return 'COMMA';
|
||||
\# return 'BRKT';
|
||||
"#" return 'BRKT';
|
||||
"," return 'COMMA';
|
||||
":::" return 'STYLE_SEPARATOR';
|
||||
":" return 'COLON';
|
||||
<block>\s+ /* skip whitespace in block */
|
||||
<block>\b((?:PK)|(?:FK)|(?:UK))\b return 'ATTRIBUTE_KEY'
|
||||
<block>(.*?)[~](.*?)*[~] return 'ATTRIBUTE_WORD';
|
||||
<block>[\*A-Za-z_][A-Za-z0-9\-_\[\]\(\)]* return 'ATTRIBUTE_WORD'
|
||||
<block>([^\s]*)[~].*[~]([^\s]*) return 'ATTRIBUTE_WORD';
|
||||
<block>([\*A-Za-z_\u00C0-\uFFFF][A-Za-z0-9\-\_\[\]\(\)\u00C0-\uFFFF\*]*) return 'ATTRIBUTE_WORD';
|
||||
<block>\"[^"]*\" return 'COMMENT';
|
||||
<block>[\n]+ /* nothing */
|
||||
<block>"}" { this.popState(); return 'BLOCK_STOP'; }
|
||||
@@ -33,6 +42,14 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multili
|
||||
"[" return 'SQS';
|
||||
"]" return 'SQE';
|
||||
|
||||
"style" { this.begin("style"); return 'STYLE'; }
|
||||
<style>[\n]+ { this.popState(); return 'NEWLINE'; }
|
||||
<style>\s+ /* skip whitespace in block */
|
||||
<style>":" return 'COLON';
|
||||
<style>"," return 'COMMA';
|
||||
<style>"#" return 'BRKT';
|
||||
"classDef" { this.begin("style"); return 'CLASSDEF'; }
|
||||
"class" return 'CLASS';
|
||||
"one or zero" return 'ZERO_OR_ONE';
|
||||
"one or more" return 'ONE_OR_MORE';
|
||||
"one or many" return 'ONE_OR_MORE';
|
||||
@@ -61,7 +78,10 @@ o\{ return 'ZERO_OR_MORE';
|
||||
"optionally to" return 'NON_IDENTIFYING';
|
||||
\.\- return 'NON_IDENTIFYING';
|
||||
\-\. return 'NON_IDENTIFYING';
|
||||
[A-Za-z_][A-Za-z0-9\-_]* return 'ALPHANUM';
|
||||
<style>([^\x00-\x7F]|\w|\-|\*)+ return 'STYLE_TEXT';
|
||||
<style>';' return 'SEMI';
|
||||
([^\x00-\x7F]|\w|\-|\*)+ return 'UNICODE_TEXT';
|
||||
[0-9] return 'NUM';
|
||||
. return yytext[0];
|
||||
<<EOF>> return 'EOF';
|
||||
|
||||
@@ -88,35 +108,126 @@ line
|
||||
|
||||
|
||||
statement
|
||||
: entityName relSpec entityName ':' role
|
||||
: entityName relSpec entityName COLON role
|
||||
{
|
||||
yy.addEntity($1);
|
||||
yy.addEntity($3);
|
||||
yy.addRelationship($1, $5, $3, $2);
|
||||
}
|
||||
| entityName STYLE_SEPARATOR idList relSpec entityName STYLE_SEPARATOR idList COLON role
|
||||
{
|
||||
yy.addEntity($1);
|
||||
yy.addEntity($5);
|
||||
yy.addRelationship($1, $9, $5, $4);
|
||||
yy.setClass([$1], $3);
|
||||
yy.setClass([$5], $7);
|
||||
}
|
||||
| entityName STYLE_SEPARATOR idList relSpec entityName COLON role
|
||||
{
|
||||
yy.addEntity($1);
|
||||
yy.addEntity($5);
|
||||
yy.addRelationship($1, $7, $5, $4);
|
||||
yy.setClass([$1], $3);
|
||||
}
|
||||
| entityName relSpec entityName STYLE_SEPARATOR idList COLON role
|
||||
{
|
||||
yy.addEntity($1);
|
||||
yy.addEntity($3);
|
||||
yy.addRelationship($1, $7, $3, $2);
|
||||
yy.setClass([$3], $5);
|
||||
}
|
||||
| entityName BLOCK_START attributes BLOCK_STOP
|
||||
{
|
||||
yy.addEntity($1);
|
||||
yy.addAttributes($1, $3);
|
||||
}
|
||||
| entityName STYLE_SEPARATOR idList BLOCK_START attributes BLOCK_STOP
|
||||
{
|
||||
yy.addEntity($1);
|
||||
yy.addAttributes($1, $5);
|
||||
yy.setClass([$1], $3);
|
||||
}
|
||||
| entityName BLOCK_START BLOCK_STOP { yy.addEntity($1); }
|
||||
| entityName STYLE_SEPARATOR idList BLOCK_START BLOCK_STOP { yy.addEntity($1); yy.setClass([$1], $3); }
|
||||
| entityName { yy.addEntity($1); }
|
||||
| entityName STYLE_SEPARATOR idList { yy.addEntity($1); yy.setClass([$1], $3); }
|
||||
| entityName SQS entityName SQE BLOCK_START attributes BLOCK_STOP
|
||||
{
|
||||
yy.addEntity($1, $3);
|
||||
yy.addAttributes($1, $6);
|
||||
}
|
||||
| entityName SQS entityName SQE STYLE_SEPARATOR idList BLOCK_START attributes BLOCK_STOP
|
||||
{
|
||||
yy.addEntity($1, $3);
|
||||
yy.addAttributes($1, $8);
|
||||
yy.setClass([$1], $6);
|
||||
|
||||
}
|
||||
| entityName SQS entityName SQE BLOCK_START BLOCK_STOP { yy.addEntity($1, $3); }
|
||||
| entityName SQS entityName SQE STYLE_SEPARATOR idList BLOCK_START BLOCK_STOP { yy.addEntity($1, $3); yy.setClass([$1], $6); }
|
||||
| entityName SQS entityName SQE { yy.addEntity($1, $3); }
|
||||
| entityName SQS entityName SQE STYLE_SEPARATOR idList { yy.addEntity($1, $3); yy.setClass([$1], $6); }
|
||||
| title title_value { $$=$2.trim();yy.setAccTitle($$); }
|
||||
| acc_title acc_title_value { $$=$2.trim();yy.setAccTitle($$); }
|
||||
| acc_descr acc_descr_value { $$=$2.trim();yy.setAccDescription($$); }
|
||||
| acc_descr_multiline_value { $$=$1.trim();yy.setAccDescription($$); }
|
||||
| direction
|
||||
| classDefStatement
|
||||
| classStatement
|
||||
| styleStatement
|
||||
;
|
||||
|
||||
direction
|
||||
: direction_tb
|
||||
{ yy.setDirection('TB');}
|
||||
| direction_bt
|
||||
{ yy.setDirection('BT');}
|
||||
| direction_rl
|
||||
{ yy.setDirection('RL');}
|
||||
| direction_lr
|
||||
{ yy.setDirection('LR');}
|
||||
;
|
||||
|
||||
classDefStatement
|
||||
: CLASSDEF idList stylesOpt separator {$$ = $CLASSDEF;yy.addClass($idList,$stylesOpt);}
|
||||
;
|
||||
|
||||
idList
|
||||
: UNICODE_TEXT { $$ = [$UNICODE_TEXT]; }
|
||||
| STYLE_TEXT { $$ = [$STYLE_TEXT]; }
|
||||
| idList COMMA UNICODE_TEXT = { $$ = $idList.concat([$UNICODE_TEXT]); }
|
||||
| idList COMMA STYLE_TEXT = { $$ = $idList.concat([$STYLE_TEXT]); }
|
||||
;
|
||||
|
||||
classStatement
|
||||
: CLASS idList idList {$$ = $CLASS;yy.setClass($2, $3);}
|
||||
;
|
||||
|
||||
styleStatement
|
||||
: STYLE idList stylesOpt separator {;$$ = $STYLE;yy.addCssStyles($2,$stylesOpt);}
|
||||
;
|
||||
|
||||
stylesOpt
|
||||
: style { $$ = [$style] }
|
||||
| stylesOpt COMMA style {$stylesOpt.push($style);$$ = $stylesOpt;}
|
||||
;
|
||||
|
||||
style
|
||||
: styleComponent
|
||||
| style styleComponent { $$ = $style + $styleComponent; }
|
||||
;
|
||||
|
||||
separator
|
||||
: SEMI
|
||||
| NEWLINE
|
||||
| EOF
|
||||
;
|
||||
|
||||
styleComponent: STYLE_TEXT | NUM | COLON | BRKT;
|
||||
|
||||
entityName
|
||||
: 'ALPHANUM' { $$ = $1; }
|
||||
| 'ENTITY_NAME' { $$ = $1.replace(/"/g, ''); }
|
||||
: 'ENTITY_NAME' { $$ = $1.replace(/"/g, ''); }
|
||||
| 'UNICODE_TEXT' { $$ = $1; }
|
||||
;
|
||||
|
||||
attributes
|
||||
@@ -125,10 +236,10 @@ attributes
|
||||
;
|
||||
|
||||
attribute
|
||||
: attributeType attributeName { $$ = { attributeType: $1, attributeName: $2 }; }
|
||||
| attributeType attributeName attributeKeyTypeList { $$ = { attributeType: $1, attributeName: $2, attributeKeyTypeList: $3 }; }
|
||||
| attributeType attributeName attributeComment { $$ = { attributeType: $1, attributeName: $2, attributeComment: $3 }; }
|
||||
| attributeType attributeName attributeKeyTypeList attributeComment { $$ = { attributeType: $1, attributeName: $2, attributeKeyTypeList: $3, attributeComment: $4 }; }
|
||||
: attributeType attributeName { $$ = { type: $1, name: $2 }; }
|
||||
| attributeType attributeName attributeKeyTypeList { $$ = { type: $1, name: $2, keys: $3 }; }
|
||||
| attributeType attributeName attributeComment { $$ = { type: $1, name: $2, comment: $3 }; }
|
||||
| attributeType attributeName attributeKeyTypeList attributeComment { $$ = { type: $1, name: $2, keys: $3, comment: $4 }; }
|
||||
;
|
||||
|
||||
|
||||
@@ -142,7 +253,7 @@ attributeName
|
||||
|
||||
attributeKeyTypeList
|
||||
: attributeKeyType { $$ = [$1]; }
|
||||
| attributeKeyTypeList COMMA attributeKeyType { $1.push($3); $$ = $1; }
|
||||
| attributeKeyTypeList ',' attributeKeyType { $1.push($3); $$ = $1; }
|
||||
;
|
||||
|
||||
attributeKeyType
|
||||
@@ -177,7 +288,7 @@ relType
|
||||
role
|
||||
: 'WORD' { $$ = $1.replace(/"/g, ''); }
|
||||
| 'ENTITY_NAME' { $$ = $1.replace(/"/g, ''); }
|
||||
| 'ALPHANUM' { $$ = $1; }
|
||||
| 'UNICODE_TEXT' { $$ = $1; }
|
||||
;
|
||||
|
||||
%%
|
||||
%%
|
||||
@@ -1,5 +1,5 @@
|
||||
import { setConfig } from '../../../config.js';
|
||||
import erDb from '../erDb.js';
|
||||
import { ErDB } from '../erDb.js';
|
||||
import erDiagram from './erDiagram.jison'; // jison file
|
||||
|
||||
setConfig({
|
||||
@@ -7,6 +7,7 @@ setConfig({
|
||||
});
|
||||
|
||||
describe('when parsing ER diagram it...', function () {
|
||||
const erDb = new ErDB();
|
||||
beforeEach(function () {
|
||||
erDiagram.parser.yy = erDb;
|
||||
erDiagram.parser.yy.clear();
|
||||
@@ -143,32 +144,32 @@ describe('when parsing ER diagram it...', function () {
|
||||
expect(entities.get(entity).alias).toBe(alias);
|
||||
});
|
||||
|
||||
it('can have an alias even if the relationship is defined before class', function () {
|
||||
it('can have an alias even if the relationship is defined before buzz', function () {
|
||||
const firstEntity = 'foo';
|
||||
const secondEntity = 'bar';
|
||||
const alias = 'batman';
|
||||
erDiagram.parser.parse(
|
||||
`erDiagram\n${firstEntity} ||--o| ${secondEntity} : rel\nclass ${firstEntity}["${alias}"]\n`
|
||||
`erDiagram\n${firstEntity} ||--o| ${secondEntity} : rel\nbuzz ${firstEntity}["${alias}"]\n`
|
||||
);
|
||||
const entities = erDb.getEntities();
|
||||
expect(entities.has(firstEntity)).toBe(true);
|
||||
expect(entities.has(secondEntity)).toBe(true);
|
||||
expect(entities.get(firstEntity).alias).toBe(alias);
|
||||
expect(entities.get(secondEntity).alias).toBeUndefined();
|
||||
expect(entities.get(secondEntity).alias).toBe('');
|
||||
});
|
||||
|
||||
it('can have an alias even if the relationship is defined after class', function () {
|
||||
it('can have an alias even if the relationship is defined after buzz', function () {
|
||||
const firstEntity = 'foo';
|
||||
const secondEntity = 'bar';
|
||||
const alias = 'batman';
|
||||
erDiagram.parser.parse(
|
||||
`erDiagram\nclass ${firstEntity}["${alias}"]\n${firstEntity} ||--o| ${secondEntity} : rel\n`
|
||||
`erDiagram\nbuzz ${firstEntity}["${alias}"]\n${firstEntity} ||--o| ${secondEntity} : rel\n`
|
||||
);
|
||||
const entities = erDb.getEntities();
|
||||
expect(entities.has(firstEntity)).toBe(true);
|
||||
expect(entities.has(secondEntity)).toBe(true);
|
||||
expect(entities.get(firstEntity).alias).toBe(alias);
|
||||
expect(entities.get(secondEntity).alias).toBeUndefined();
|
||||
expect(entities.get(secondEntity).alias).toBe('');
|
||||
});
|
||||
|
||||
it('can start with an underscore', function () {
|
||||
@@ -193,9 +194,9 @@ describe('when parsing ER diagram it...', function () {
|
||||
|
||||
expect(entities.size).toBe(1);
|
||||
expect(entities.get(entity).attributes.length).toBe(3);
|
||||
expect(entities.get(entity).attributes[0].attributeName).toBe('myBookTitle');
|
||||
expect(entities.get(entity).attributes[1].attributeName).toBe('MYBOOKSUBTITLE_1');
|
||||
expect(entities.get(entity).attributes[2].attributeName).toBe('author-ref[name](1)');
|
||||
expect(entities.get(entity).attributes[0].name).toBe('myBookTitle');
|
||||
expect(entities.get(entity).attributes[1].name).toBe('MYBOOKSUBTITLE_1');
|
||||
expect(entities.get(entity).attributes[2].name).toBe('author-ref[name](1)');
|
||||
});
|
||||
|
||||
it('should allow asterisk at the start of attribute name', function () {
|
||||
@@ -258,7 +259,7 @@ describe('when parsing ER diagram it...', function () {
|
||||
const entities = erDb.getEntities();
|
||||
expect(entities.size).toBe(1);
|
||||
expect(entities.get(entity).attributes.length).toBe(1);
|
||||
expect(entities.get(entity).attributes[0].attributeComment).toBe('comment');
|
||||
expect(entities.get(entity).attributes[0].comment).toBe('comment');
|
||||
});
|
||||
|
||||
it('should allow an entity with a single attribute to be defined with a key and a comment', function () {
|
||||
@@ -297,14 +298,14 @@ describe('when parsing ER diagram it...', function () {
|
||||
`erDiagram\n${entity} {\n${attribute1}\n${attribute2}\n${attribute3}\n${attribute4}\n${attribute5}\n}`
|
||||
);
|
||||
const entities = erDb.getEntities();
|
||||
expect(entities.get(entity).attributes[0].attributeKeyTypeList).toEqual(['PK', 'FK']);
|
||||
expect(entities.get(entity).attributes[0].attributeComment).toBe('comment1');
|
||||
expect(entities.get(entity).attributes[1].attributeKeyTypeList).toEqual(['PK', 'UK', 'FK']);
|
||||
expect(entities.get(entity).attributes[2].attributeKeyTypeList).toEqual(['PK', 'UK']);
|
||||
expect(entities.get(entity).attributes[2].attributeComment).toBe('comment3');
|
||||
expect(entities.get(entity).attributes[3].attributeKeyTypeList).toBeUndefined();
|
||||
expect(entities.get(entity).attributes[4].attributeKeyTypeList).toBeUndefined();
|
||||
expect(entities.get(entity).attributes[4].attributeComment).toBe('comment5');
|
||||
expect(entities.get(entity).attributes[0].keys).toEqual(['PK', 'FK']);
|
||||
expect(entities.get(entity).attributes[0].comment).toBe('comment1');
|
||||
expect(entities.get(entity).attributes[1].keys).toEqual(['PK', 'UK', 'FK']);
|
||||
expect(entities.get(entity).attributes[2].keys).toEqual(['PK', 'UK']);
|
||||
expect(entities.get(entity).attributes[2].comment).toBe('comment3');
|
||||
expect(entities.get(entity).attributes[3].keys).toEqual([]);
|
||||
expect(entities.get(entity).attributes[4].keys).toEqual([]);
|
||||
expect(entities.get(entity).attributes[4].comment).toBe('comment5');
|
||||
});
|
||||
|
||||
it('should allow an entity with attribute that has a generic type', function () {
|
||||
@@ -341,8 +342,8 @@ describe('when parsing ER diagram it...', function () {
|
||||
const entities = erDb.getEntities();
|
||||
expect(entities.size).toBe(1);
|
||||
expect(entities.get(entity).attributes.length).toBe(2);
|
||||
expect(entities.get(entity).attributes[0].attributeType).toBe('character(10)');
|
||||
expect(entities.get(entity).attributes[1].attributeType).toBe('varchar(5)');
|
||||
expect(entities.get(entity).attributes[0].type).toBe('character(10)');
|
||||
expect(entities.get(entity).attributes[1].type).toBe('varchar(5)');
|
||||
});
|
||||
|
||||
it('should allow an entity with multiple attributes to be defined', function () {
|
||||
@@ -764,6 +765,203 @@ describe('when parsing ER diagram it...', function () {
|
||||
}).toThrowError();
|
||||
});
|
||||
|
||||
it('should be possible to apply a style to an entity', function () {
|
||||
const entityName = 'CUSTOMER';
|
||||
erDiagram.parser.parse(`erDiagram
|
||||
${entityName}
|
||||
style ${entityName} color:red
|
||||
`);
|
||||
|
||||
expect(erDb.getEntity(entityName).cssStyles).toEqual(['color:red']);
|
||||
});
|
||||
|
||||
it('should be possible to apply multiple styles to an entity at the same time', function () {
|
||||
const entityName = 'CUSTOMER';
|
||||
erDiagram.parser.parse(
|
||||
`erDiagram
|
||||
${entityName}
|
||||
style ${entityName} color:red,stroke:blue,fill:#f9f
|
||||
`
|
||||
);
|
||||
|
||||
expect(erDb.getEntity(entityName).cssStyles).toEqual(['color:red', 'stroke:blue', 'fill:#f9f']);
|
||||
});
|
||||
|
||||
it('should be possible to apply multiple separately defined styles', function () {
|
||||
const entityName = 'CUSTOMER';
|
||||
erDiagram.parser.parse(
|
||||
`erDiagram
|
||||
${entityName}
|
||||
style ${entityName} color:red
|
||||
style ${entityName} fill:#f9f
|
||||
`
|
||||
);
|
||||
|
||||
expect(erDb.getEntity(entityName).cssStyles).toEqual(['color:red', 'fill:#f9f']);
|
||||
});
|
||||
|
||||
it('should be possible to assign a class to an entity', function () {
|
||||
const entityName = 'CUSTOMER';
|
||||
erDiagram.parser.parse(`erDiagram\n${entityName}\nclass ${entityName} myClass`);
|
||||
|
||||
expect(erDb.getEntity(entityName).cssClasses).toBe('default myClass');
|
||||
});
|
||||
|
||||
it('should be possible to assign multiple classes to an entity at the same time', function () {
|
||||
const entityName = 'CUSTOMER';
|
||||
erDiagram.parser.parse(
|
||||
`erDiagram\n${entityName}\nclass ${entityName} firstClass, secondClass, thirdClass`
|
||||
);
|
||||
|
||||
expect(erDb.getEntity(entityName).cssClasses).toBe('default firstClass secondClass thirdClass');
|
||||
});
|
||||
|
||||
it('should be possible to assign multiple separately defined classes to an entity', function () {
|
||||
const entityName = 'CUSTOMER';
|
||||
erDiagram.parser.parse(
|
||||
`erDiagram\n${entityName}\nclass ${entityName} firstClass\nclass ${entityName} secondClass`
|
||||
);
|
||||
|
||||
expect(erDb.getEntity(entityName).cssClasses).toBe('default firstClass secondClass');
|
||||
});
|
||||
|
||||
it('should be possible to configure the default class and have it apply to each entity', function () {
|
||||
const firstEntity = 'ENTITY1';
|
||||
const secondEntity = 'ENTITY2';
|
||||
erDiagram.parser.parse(
|
||||
`erDiagram
|
||||
${firstEntity}
|
||||
${secondEntity}
|
||||
classDef default fill:#f9f
|
||||
`
|
||||
);
|
||||
|
||||
const expectedOutput = new Map([
|
||||
[
|
||||
'default',
|
||||
{
|
||||
id: 'default',
|
||||
styles: ['fill:#f9f'],
|
||||
textStyles: [],
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
expect(erDb.getEntity(firstEntity).cssClasses).toBe('default');
|
||||
expect(erDb.getEntity(secondEntity).cssClasses).toBe('default');
|
||||
expect(erDb.getClasses()).toEqual(expectedOutput);
|
||||
});
|
||||
|
||||
it('should be possible to define a class with styles', function () {
|
||||
const className = 'myClass';
|
||||
const styles = 'fill:#f9f, stroke: red, color: pink';
|
||||
erDiagram.parser.parse(
|
||||
`erDiagram
|
||||
classDef ${className} ${styles}
|
||||
`
|
||||
);
|
||||
|
||||
const expectedOutput = new Map([
|
||||
[
|
||||
className,
|
||||
{
|
||||
id: className,
|
||||
styles: ['fill:#f9f', 'stroke:red', 'color:pink'],
|
||||
textStyles: ['color:pink'],
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
expect(erDb.getClasses()).toEqual(expectedOutput);
|
||||
});
|
||||
|
||||
it('should be possible to define multiple class with styles at the same time', function () {
|
||||
const firstClass = 'firstClass';
|
||||
const secondClass = 'secondClass';
|
||||
const styles = 'fill:#f9f, stroke: red, color: pink';
|
||||
erDiagram.parser.parse(
|
||||
`erDiagram
|
||||
classDef ${firstClass},${secondClass} ${styles}
|
||||
`
|
||||
);
|
||||
|
||||
const expectedOutput = new Map([
|
||||
[
|
||||
firstClass,
|
||||
{
|
||||
id: firstClass,
|
||||
styles: ['fill:#f9f', 'stroke:red', 'color:pink'],
|
||||
textStyles: ['color:pink'],
|
||||
},
|
||||
],
|
||||
[
|
||||
secondClass,
|
||||
{
|
||||
id: secondClass,
|
||||
styles: ['fill:#f9f', 'stroke:red', 'color:pink'],
|
||||
textStyles: ['color:pink'],
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
expect(erDb.getClasses()).toEqual(expectedOutput);
|
||||
});
|
||||
|
||||
it('should be possible to assign a class using the shorthand syntax just by itself', function () {
|
||||
const entityName = 'CUSTOMER';
|
||||
const className = 'myClass';
|
||||
erDiagram.parser.parse(`erDiagram\n${entityName}:::${className}`);
|
||||
|
||||
expect(erDb.getEntity(entityName).cssClasses).toBe('default myClass');
|
||||
});
|
||||
|
||||
it('should be possible to assign a class using the shorthand syntax with empty block', function () {
|
||||
const entityName = 'CUSTOMER';
|
||||
const className = 'myClass';
|
||||
erDiagram.parser.parse(`erDiagram\n${entityName}:::${className} {}`);
|
||||
|
||||
expect(erDb.getEntity(entityName).cssClasses).toBe('default myClass');
|
||||
});
|
||||
|
||||
it('should be possible to assign a class using the shorthand syntax with block of attributes', function () {
|
||||
const entityName = 'CUSTOMER';
|
||||
const className = 'myClass';
|
||||
erDiagram.parser.parse(`erDiagram\n${entityName}:::${className} {\nstring name\n}`);
|
||||
|
||||
expect(erDb.getEntity(entityName).cssClasses).toBe('default myClass');
|
||||
});
|
||||
|
||||
it('should be possible to assign multiple classes using the shorthand syntax', function () {
|
||||
const entityName = 'CUSTOMER';
|
||||
const firstClass = 'firstClass';
|
||||
const secondClass = 'secondClass';
|
||||
erDiagram.parser.parse(`erDiagram\n${entityName}:::${firstClass},${secondClass}`);
|
||||
|
||||
expect(erDb.getEntity(entityName).cssClasses).toBe('default firstClass secondClass');
|
||||
});
|
||||
|
||||
it('should be possible to assign classes using the shorthand syntax after defining an alias', function () {
|
||||
const entityName = 'c';
|
||||
const entityAlias = 'CUSTOMER';
|
||||
const myClass = 'myClass';
|
||||
erDiagram.parser.parse(`erDiagram\n${entityName}[${entityAlias}]:::${myClass}`);
|
||||
|
||||
expect(erDb.getEntity(entityName).alias).toBe(entityAlias);
|
||||
expect(erDb.getEntity(entityName).cssClasses).toBe('default myClass');
|
||||
});
|
||||
|
||||
it('should be possible to assign classes using the shorthand syntax while defining a relationship', function () {
|
||||
const entityName = 'CUSTOMER';
|
||||
const otherEntity = 'PERSON';
|
||||
const myClass = 'myClass';
|
||||
erDiagram.parser.parse(
|
||||
`erDiagram\n${entityName}:::${myClass} ||--o{ ${otherEntity}:::${myClass} : allows`
|
||||
);
|
||||
|
||||
expect(erDb.getEntity(entityName).cssClasses).toBe('default myClass');
|
||||
expect(erDb.getEntity(otherEntity).cssClasses).toBe('default myClass');
|
||||
});
|
||||
|
||||
describe('relationship labels', function () {
|
||||
it('should allow an empty quoted label', function () {
|
||||
erDiagram.parser.parse('erDiagram\nCUSTOMER ||--|{ ORDER : ""');
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
const getStyles = (options) =>
|
||||
`
|
||||
.entityBox {
|
||||
fill: ${options.mainBkg};
|
||||
stroke: ${options.nodeBorder};
|
||||
}
|
||||
|
||||
.attributeBoxOdd {
|
||||
fill: ${options.attributeBackgroundColorOdd};
|
||||
stroke: ${options.nodeBorder};
|
||||
}
|
||||
|
||||
.attributeBoxEven {
|
||||
fill: ${options.attributeBackgroundColorEven};
|
||||
stroke: ${options.nodeBorder};
|
||||
}
|
||||
|
||||
.relationshipLabelBox {
|
||||
fill: ${options.tertiaryColor};
|
||||
opacity: 0.7;
|
||||
background-color: ${options.tertiaryColor};
|
||||
rect {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.relationshipLine {
|
||||
stroke: ${options.lineColor};
|
||||
}
|
||||
|
||||
.entityTitleText {
|
||||
text-anchor: middle;
|
||||
font-size: 18px;
|
||||
fill: ${options.textColor};
|
||||
}
|
||||
#MD_PARENT_START {
|
||||
fill: #f5f5f5 !important;
|
||||
stroke: ${options.lineColor} !important;
|
||||
stroke-width: 1;
|
||||
}
|
||||
#MD_PARENT_END {
|
||||
fill: #f5f5f5 !important;
|
||||
stroke: ${options.lineColor} !important;
|
||||
stroke-width: 1;
|
||||
}
|
||||
|
||||
`;
|
||||
|
||||
export default getStyles;
|
||||
73
packages/mermaid/src/diagrams/er/styles.ts
Normal file
73
packages/mermaid/src/diagrams/er/styles.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import * as khroma from 'khroma';
|
||||
import type { FlowChartStyleOptions } from '../flowchart/styles.js';
|
||||
|
||||
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: FlowChartStyleOptions) =>
|
||||
`
|
||||
.entityBox {
|
||||
fill: ${options.mainBkg};
|
||||
stroke: ${options.nodeBorder};
|
||||
}
|
||||
|
||||
.relationshipLabelBox {
|
||||
fill: ${options.tertiaryColor};
|
||||
opacity: 0.7;
|
||||
background-color: ${options.tertiaryColor};
|
||||
rect {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.labelBkg {
|
||||
background-color: ${fade(options.tertiaryColor, 0.5)};
|
||||
}
|
||||
|
||||
.edgeLabel .label {
|
||||
fill: ${options.nodeBorder};
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-family: ${options.fontFamily};
|
||||
color: ${options.nodeTextColor || options.textColor};
|
||||
}
|
||||
|
||||
.edge-pattern-dashed {
|
||||
stroke-dasharray: 8,8;
|
||||
}
|
||||
|
||||
.node rect,
|
||||
.node circle,
|
||||
.node ellipse,
|
||||
.node polygon
|
||||
{
|
||||
fill: ${options.mainBkg};
|
||||
stroke: ${options.nodeBorder};
|
||||
stroke-width: 1px;
|
||||
}
|
||||
|
||||
.relationshipLine {
|
||||
stroke: ${options.lineColor};
|
||||
stroke-width: 1;
|
||||
fill: none;
|
||||
}
|
||||
|
||||
.marker {
|
||||
fill: none !important;
|
||||
stroke: ${options.lineColor} !important;
|
||||
stroke-width: 1;
|
||||
}
|
||||
`;
|
||||
|
||||
export default getStyles;
|
||||
@@ -1,9 +1,11 @@
|
||||
import flowDb from './flowDb.js';
|
||||
import { FlowDB } from './flowDb.js';
|
||||
import type { FlowSubGraph } from './types.js';
|
||||
|
||||
describe('flow db subgraphs', () => {
|
||||
let flowDb: FlowDB;
|
||||
let subgraphs: FlowSubGraph[];
|
||||
beforeEach(() => {
|
||||
flowDb = new FlowDB();
|
||||
subgraphs = [
|
||||
{ nodes: ['a', 'b', 'c', 'e'] },
|
||||
{ nodes: ['f', 'g', 'h'] },
|
||||
@@ -44,8 +46,9 @@ describe('flow db subgraphs', () => {
|
||||
});
|
||||
|
||||
describe('flow db addClass', () => {
|
||||
let flowDb: FlowDB;
|
||||
beforeEach(() => {
|
||||
flowDb.clear();
|
||||
flowDb = new FlowDB();
|
||||
});
|
||||
it('should detect many classes', () => {
|
||||
flowDb.addClass('a,b', ['stroke-width: 8px']);
|
||||
@@ -65,3 +68,33 @@ describe('flow db addClass', () => {
|
||||
expect(classes.get('a')?.styles).toEqual(['stroke-width: 8px']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('flow db class', () => {
|
||||
let flowDb: FlowDB;
|
||||
beforeEach(() => {
|
||||
flowDb = new FlowDB();
|
||||
});
|
||||
// This is to ensure that functions used in flow JISON are exposed as function from FlowDB
|
||||
it('should have functions used in flow JISON as own property', () => {
|
||||
const functionsUsedInParser = [
|
||||
'setDirection',
|
||||
'addSubGraph',
|
||||
'setAccTitle',
|
||||
'setAccDescription',
|
||||
'addVertex',
|
||||
'addLink',
|
||||
'setClass',
|
||||
'destructLink',
|
||||
'addClass',
|
||||
'setClickEvent',
|
||||
'setTooltip',
|
||||
'setLink',
|
||||
'updateLink',
|
||||
'updateLinkInterpolate',
|
||||
] as const satisfies (keyof FlowDB)[];
|
||||
|
||||
for (const fun of functionsUsedInParser) {
|
||||
expect(Object.hasOwn(flowDb, fun)).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,14 +1,17 @@
|
||||
import type { MermaidConfig } from '../../config.type.js';
|
||||
import { setConfig } from '../../diagram-api/diagramAPI.js';
|
||||
import flowDb from './flowDb.js';
|
||||
import { FlowDB } from './flowDb.js';
|
||||
import renderer from './flowRenderer-v3-unified.js';
|
||||
// @ts-ignore: JISON doesn't support types
|
||||
import flowParser from './parser/flow.jison';
|
||||
//import flowParser from './parser/flow.jison';
|
||||
import flowParser from './parser/flowParser.ts';
|
||||
import flowStyles from './styles.js';
|
||||
|
||||
export const diagram = {
|
||||
parser: flowParser,
|
||||
db: flowDb,
|
||||
get db() {
|
||||
return new FlowDB();
|
||||
},
|
||||
renderer,
|
||||
styles: flowStyles,
|
||||
init: (cnf: MermaidConfig) => {
|
||||
@@ -20,7 +23,5 @@ export const diagram = {
|
||||
}
|
||||
cnf.flowchart.arrowMarkerAbsolute = cnf.arrowMarkerAbsolute;
|
||||
setConfig({ flowchart: { arrowMarkerAbsolute: cnf.arrowMarkerAbsolute } });
|
||||
flowDb.clear();
|
||||
flowDb.setGen('gen-2');
|
||||
},
|
||||
};
|
||||
|
||||
@@ -7,7 +7,6 @@ import { getRegisteredLayoutAlgorithm, render } from '../../rendering-util/rende
|
||||
import { setupViewPortForSVG } from '../../rendering-util/setupViewPortForSVG.js';
|
||||
import type { LayoutData } from '../../rendering-util/types.js';
|
||||
import utils from '../../utils.js';
|
||||
import { getDirection } from './flowDb.js';
|
||||
|
||||
export const getClasses = function (
|
||||
text: string,
|
||||
@@ -37,7 +36,7 @@ export const draw = async function (text: string, id: string, _version: string,
|
||||
log.debug('Data: ', data4Layout);
|
||||
// Create the root SVG
|
||||
const svg = getDiagramElement(id, securityLevel);
|
||||
const direction = getDirection();
|
||||
const direction = diag.db.getDirection();
|
||||
|
||||
data4Layout.type = diag.type;
|
||||
data4Layout.layoutAlgorithm = getRegisteredLayoutAlgorithm(layout);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import flowDb from '../flowDb.js';
|
||||
import flow from './flow.jison';
|
||||
import { FlowDB } from '../flowDb.js';
|
||||
import flow from './flowParser.ts';
|
||||
import { setConfig } from '../../../config.js';
|
||||
|
||||
setConfig({
|
||||
@@ -8,7 +8,7 @@ setConfig({
|
||||
|
||||
describe('[Arrows] when parsing', () => {
|
||||
beforeEach(function () {
|
||||
flow.parser.yy = flowDb;
|
||||
flow.parser.yy = new FlowDB();
|
||||
flow.parser.yy.clear();
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import flowDb from '../flowDb.js';
|
||||
import flow from './flow.jison';
|
||||
import { FlowDB } from '../flowDb.js';
|
||||
import flow from './flowParser.ts';
|
||||
import { setConfig } from '../../../config.js';
|
||||
import { cleanupComments } from '../../../diagram-api/comments.js';
|
||||
|
||||
@@ -9,7 +9,7 @@ setConfig({
|
||||
|
||||
describe('[Comments] when parsing', () => {
|
||||
beforeEach(function () {
|
||||
flow.parser.yy = flowDb;
|
||||
flow.parser.yy = new FlowDB();
|
||||
flow.parser.yy.clear();
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import flowDb from '../flowDb.js';
|
||||
import flow from './flow.jison';
|
||||
import { FlowDB } from '../flowDb.js';
|
||||
import flow from './flowParser.ts';
|
||||
import { setConfig } from '../../../config.js';
|
||||
|
||||
setConfig({
|
||||
@@ -8,7 +8,7 @@ setConfig({
|
||||
|
||||
describe('when parsing directions', function () {
|
||||
beforeEach(function () {
|
||||
flow.parser.yy = flowDb;
|
||||
flow.parser.yy = new FlowDB();
|
||||
flow.parser.yy.clear();
|
||||
flow.parser.yy.setGen('gen-2');
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import flowDb from '../flowDb.js';
|
||||
import flow from './flow.jison';
|
||||
import { FlowDB } from '../flowDb.js';
|
||||
import flow from './flowParser.ts';
|
||||
import { setConfig } from '../../../config.js';
|
||||
|
||||
setConfig({
|
||||
@@ -39,10 +39,31 @@ const doubleEndedEdges = [
|
||||
{ edgeStart: '<==', edgeEnd: '==>', stroke: 'thick', type: 'double_arrow_point' },
|
||||
{ edgeStart: '<-.', edgeEnd: '.->', stroke: 'dotted', type: 'double_arrow_point' },
|
||||
];
|
||||
const regularEdges = [
|
||||
{ edgeStart: '--', edgeEnd: '--x', stroke: 'normal', type: 'arrow_cross' },
|
||||
{ edgeStart: '==', edgeEnd: '==x', stroke: 'thick', type: 'arrow_cross' },
|
||||
{ edgeStart: '-.', edgeEnd: '.-x', stroke: 'dotted', type: 'arrow_cross' },
|
||||
{ edgeStart: '--', edgeEnd: '--o', stroke: 'normal', type: 'arrow_circle' },
|
||||
{ edgeStart: '==', edgeEnd: '==o', stroke: 'thick', type: 'arrow_circle' },
|
||||
{ edgeStart: '-.', edgeEnd: '.-o', stroke: 'dotted', type: 'arrow_circle' },
|
||||
{ edgeStart: '--', edgeEnd: '-->', stroke: 'normal', type: 'arrow_point' },
|
||||
{ edgeStart: '==', edgeEnd: '==>', stroke: 'thick', type: 'arrow_point' },
|
||||
{ edgeStart: '-.', edgeEnd: '.->', stroke: 'dotted', type: 'arrow_point' },
|
||||
|
||||
{ edgeStart: '--', edgeEnd: '----x', stroke: 'normal', type: 'arrow_cross' },
|
||||
{ edgeStart: '==', edgeEnd: '====x', stroke: 'thick', type: 'arrow_cross' },
|
||||
{ edgeStart: '-.', edgeEnd: '...-x', stroke: 'dotted', type: 'arrow_cross' },
|
||||
{ edgeStart: '--', edgeEnd: '----o', stroke: 'normal', type: 'arrow_circle' },
|
||||
{ edgeStart: '==', edgeEnd: '====o', stroke: 'thick', type: 'arrow_circle' },
|
||||
{ edgeStart: '-.', edgeEnd: '...-o', stroke: 'dotted', type: 'arrow_circle' },
|
||||
{ edgeStart: '--', edgeEnd: '---->', stroke: 'normal', type: 'arrow_point' },
|
||||
{ edgeStart: '==', edgeEnd: '====>', stroke: 'thick', type: 'arrow_point' },
|
||||
{ edgeStart: '-.', edgeEnd: '...->', stroke: 'dotted', type: 'arrow_point' },
|
||||
];
|
||||
|
||||
describe('[Edges] when parsing', () => {
|
||||
beforeEach(function () {
|
||||
flow.parser.yy = flowDb;
|
||||
flow.parser.yy = new FlowDB();
|
||||
flow.parser.yy.clear();
|
||||
});
|
||||
|
||||
@@ -67,6 +88,74 @@ describe('[Edges] when parsing', () => {
|
||||
expect(edges[0].type).toBe('arrow_circle');
|
||||
});
|
||||
|
||||
describe('edges with ids', function () {
|
||||
describe('open ended edges with ids and labels', function () {
|
||||
regularEdges.forEach((edgeType) => {
|
||||
it(`should handle ${edgeType.stroke} ${edgeType.type} with no text`, function () {
|
||||
const res = flow.parser.parse(
|
||||
`flowchart TD;\nA e1@${edgeType.edgeStart}${edgeType.edgeEnd} B;`
|
||||
);
|
||||
const vert = flow.parser.yy.getVertices();
|
||||
const edges = flow.parser.yy.getEdges();
|
||||
expect(vert.get('A').id).toBe('A');
|
||||
expect(vert.get('B').id).toBe('B');
|
||||
expect(edges.length).toBe(1);
|
||||
expect(edges[0].id).toBe('e1');
|
||||
expect(edges[0].start).toBe('A');
|
||||
expect(edges[0].end).toBe('B');
|
||||
expect(edges[0].type).toBe(`${edgeType.type}`);
|
||||
expect(edges[0].text).toBe('');
|
||||
expect(edges[0].stroke).toBe(`${edgeType.stroke}`);
|
||||
});
|
||||
it(`should handle ${edgeType.stroke} ${edgeType.type} with text`, function () {
|
||||
const res = flow.parser.parse(
|
||||
`flowchart TD;\nA e1@${edgeType.edgeStart}${edgeType.edgeEnd} B;`
|
||||
);
|
||||
const vert = flow.parser.yy.getVertices();
|
||||
const edges = flow.parser.yy.getEdges();
|
||||
expect(vert.get('A').id).toBe('A');
|
||||
expect(vert.get('B').id).toBe('B');
|
||||
expect(edges.length).toBe(1);
|
||||
expect(edges[0].id).toBe('e1');
|
||||
expect(edges[0].start).toBe('A');
|
||||
expect(edges[0].end).toBe('B');
|
||||
expect(edges[0].type).toBe(`${edgeType.type}`);
|
||||
expect(edges[0].text).toBe('');
|
||||
expect(edges[0].stroke).toBe(`${edgeType.stroke}`);
|
||||
});
|
||||
});
|
||||
it('should handle normal edges where you also have a node with metadata', function () {
|
||||
const res = flow.parser.parse(`flowchart LR
|
||||
A id1@-->B
|
||||
A@{ shape: 'rect' }
|
||||
`);
|
||||
const edges = flow.parser.yy.getEdges();
|
||||
|
||||
expect(edges[0].id).toBe('id1');
|
||||
});
|
||||
});
|
||||
describe('double ended edges with ids and labels', function () {
|
||||
doubleEndedEdges.forEach((edgeType) => {
|
||||
it(`should handle ${edgeType.stroke} ${edgeType.type} with text`, function () {
|
||||
const res = flow.parser.parse(
|
||||
`flowchart TD;\nA e1@${edgeType.edgeStart} label ${edgeType.edgeEnd} B;`
|
||||
);
|
||||
const vert = flow.parser.yy.getVertices();
|
||||
const edges = flow.parser.yy.getEdges();
|
||||
expect(vert.get('A').id).toBe('A');
|
||||
expect(vert.get('B').id).toBe('B');
|
||||
expect(edges.length).toBe(1);
|
||||
expect(edges[0].id).toBe('e1');
|
||||
expect(edges[0].start).toBe('A');
|
||||
expect(edges[0].end).toBe('B');
|
||||
expect(edges[0].type).toBe(`${edgeType.type}`);
|
||||
expect(edges[0].text).toBe('label');
|
||||
expect(edges[0].stroke).toBe(`${edgeType.stroke}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('edges', function () {
|
||||
doubleEndedEdges.forEach((edgeType) => {
|
||||
it(`should handle ${edgeType.stroke} ${edgeType.type} with no text`, function () {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import flowDb from '../flowDb.js';
|
||||
import flow from './flow.jison';
|
||||
import { FlowDB } from '../flowDb.js';
|
||||
import flow from './flowParser.ts';
|
||||
import { setConfig } from '../../../config.js';
|
||||
|
||||
setConfig({
|
||||
@@ -8,7 +8,7 @@ setConfig({
|
||||
|
||||
describe('[Text] when parsing', () => {
|
||||
beforeEach(function () {
|
||||
flow.parser.yy = flowDb;
|
||||
flow.parser.yy = new FlowDB();
|
||||
flow.parser.yy.clear();
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import flowDb from '../flowDb.js';
|
||||
import flow from './flow.jison';
|
||||
import { FlowDB } from '../flowDb.js';
|
||||
import flow from './flowParser.ts';
|
||||
import { setConfig } from '../../../config.js';
|
||||
import { vi } from 'vitest';
|
||||
const spyOn = vi.spyOn;
|
||||
@@ -9,7 +9,9 @@ setConfig({
|
||||
});
|
||||
|
||||
describe('[Interactions] when parsing', () => {
|
||||
let flowDb;
|
||||
beforeEach(function () {
|
||||
flowDb = new FlowDB();
|
||||
flow.parser.yy = flowDb;
|
||||
flow.parser.yy.clear();
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import flowDb from '../flowDb.js';
|
||||
import flow from './flow.jison';
|
||||
import { FlowDB } from '../flowDb.js';
|
||||
import flow from './flowParser.ts';
|
||||
import { setConfig } from '../../../config.js';
|
||||
|
||||
setConfig({
|
||||
@@ -8,7 +8,7 @@ setConfig({
|
||||
|
||||
describe('[Lines] when parsing', () => {
|
||||
beforeEach(function () {
|
||||
flow.parser.yy = flowDb;
|
||||
flow.parser.yy = new FlowDB();
|
||||
flow.parser.yy.clear();
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import flowDb from '../flowDb.js';
|
||||
import flow from './flow.jison';
|
||||
import { FlowDB } from '../flowDb.js';
|
||||
import flow from './flowParser.ts';
|
||||
import { setConfig } from '../../../config.js';
|
||||
|
||||
setConfig({
|
||||
@@ -8,7 +8,7 @@ setConfig({
|
||||
|
||||
describe('parsing a flow chart with markdown strings', function () {
|
||||
beforeEach(function () {
|
||||
flow.parser.yy = flowDb;
|
||||
flow.parser.yy = new FlowDB();
|
||||
flow.parser.yy.clear();
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import flowDb from '../flowDb.js';
|
||||
import flow from './flow.jison';
|
||||
import { FlowDB } from '../flowDb.js';
|
||||
import flow from './flowParser.ts';
|
||||
import { setConfig } from '../../../config.js';
|
||||
|
||||
setConfig({
|
||||
@@ -8,7 +8,7 @@ setConfig({
|
||||
|
||||
describe('when parsing directions', function () {
|
||||
beforeEach(function () {
|
||||
flow.parser.yy = flowDb;
|
||||
flow.parser.yy = new FlowDB();
|
||||
flow.parser.yy.clear();
|
||||
flow.parser.yy.setGen('gen-2');
|
||||
});
|
||||
@@ -251,7 +251,7 @@ describe('when parsing directions', function () {
|
||||
expect(data4Layout.nodes[0].shape).toEqual('squareRect');
|
||||
expect(data4Layout.nodes[0].label).toEqual('This is a<br/>multiline string');
|
||||
});
|
||||
it(' should be possible to use } in strings', function () {
|
||||
it('should be possible to use } in strings', function () {
|
||||
const res = flow.parser.parse(`flowchart TB
|
||||
A@{
|
||||
label: "This is a string with }"
|
||||
@@ -264,7 +264,7 @@ describe('when parsing directions', function () {
|
||||
expect(data4Layout.nodes[0].shape).toEqual('squareRect');
|
||||
expect(data4Layout.nodes[0].label).toEqual('This is a string with }');
|
||||
});
|
||||
it(' should be possible to use @ in strings', function () {
|
||||
it('should be possible to use @ in strings', function () {
|
||||
const res = flow.parser.parse(`flowchart TB
|
||||
A@{
|
||||
label: "This is a string with @"
|
||||
@@ -277,7 +277,7 @@ describe('when parsing directions', function () {
|
||||
expect(data4Layout.nodes[0].shape).toEqual('squareRect');
|
||||
expect(data4Layout.nodes[0].label).toEqual('This is a string with @');
|
||||
});
|
||||
it(' should be possible to use @ in strings', function () {
|
||||
it('should be possible to use @ in strings', function () {
|
||||
const res = flow.parser.parse(`flowchart TB
|
||||
A@{
|
||||
label: "This is a string with}"
|
||||
@@ -290,4 +290,126 @@ describe('when parsing directions', function () {
|
||||
expect(data4Layout.nodes[0].shape).toEqual('squareRect');
|
||||
expect(data4Layout.nodes[0].label).toEqual('This is a string with}');
|
||||
});
|
||||
|
||||
it('should be possible to use @ syntax to add labels on multi nodes', function () {
|
||||
const res = flow.parser.parse(`flowchart TB
|
||||
n2["label for n2"] & n4@{ label: "labe for n4"} & n5@{ label: "labe for n5"}
|
||||
`);
|
||||
|
||||
const data4Layout = flow.parser.yy.getData();
|
||||
expect(data4Layout.nodes.length).toBe(3);
|
||||
expect(data4Layout.nodes[0].label).toEqual('label for n2');
|
||||
expect(data4Layout.nodes[1].label).toEqual('labe for n4');
|
||||
expect(data4Layout.nodes[2].label).toEqual('labe for n5');
|
||||
});
|
||||
|
||||
it('should be possible to use @ syntax to add labels on multi nodes with edge/link', function () {
|
||||
const res = flow.parser.parse(`flowchart TD
|
||||
A["A"] --> B["for B"] & C@{ label: "for c"} & E@{label : "for E"}
|
||||
D@{label: "for D"}
|
||||
`);
|
||||
|
||||
const data4Layout = flow.parser.yy.getData();
|
||||
expect(data4Layout.nodes.length).toBe(5);
|
||||
expect(data4Layout.nodes[0].label).toEqual('A');
|
||||
expect(data4Layout.nodes[1].label).toEqual('for B');
|
||||
expect(data4Layout.nodes[2].label).toEqual('for c');
|
||||
expect(data4Layout.nodes[3].label).toEqual('for E');
|
||||
expect(data4Layout.nodes[4].label).toEqual('for D');
|
||||
});
|
||||
|
||||
it('should be possible to use @ syntax in labels', function () {
|
||||
const res = flow.parser.parse(`flowchart TD
|
||||
A["@A@"] --> B["@for@ B@"] & C@{ label: "@for@ c@"} & E{"\`@for@ E@\`"} & D(("@for@ D@"))
|
||||
H1{{"@for@ H@"}}
|
||||
H2{{"\`@for@ H@\`"}}
|
||||
Q1{"@for@ Q@"}
|
||||
Q2{"\`@for@ Q@\`"}
|
||||
AS1>"@for@ AS@"]
|
||||
AS2>"\`@for@ AS@\`"]
|
||||
`);
|
||||
|
||||
const data4Layout = flow.parser.yy.getData();
|
||||
expect(data4Layout.nodes.length).toBe(11);
|
||||
expect(data4Layout.nodes[0].label).toEqual('@A@');
|
||||
expect(data4Layout.nodes[1].label).toEqual('@for@ B@');
|
||||
expect(data4Layout.nodes[2].label).toEqual('@for@ c@');
|
||||
expect(data4Layout.nodes[3].label).toEqual('@for@ E@');
|
||||
expect(data4Layout.nodes[4].label).toEqual('@for@ D@');
|
||||
expect(data4Layout.nodes[5].label).toEqual('@for@ H@');
|
||||
expect(data4Layout.nodes[6].label).toEqual('@for@ H@');
|
||||
expect(data4Layout.nodes[7].label).toEqual('@for@ Q@');
|
||||
expect(data4Layout.nodes[8].label).toEqual('@for@ Q@');
|
||||
expect(data4Layout.nodes[9].label).toEqual('@for@ AS@');
|
||||
expect(data4Layout.nodes[10].label).toEqual('@for@ AS@');
|
||||
});
|
||||
|
||||
it('should handle unique edge creation with using @ and &', function () {
|
||||
const res = flow.parser.parse(`flowchart TD
|
||||
A & B e1@--> C & D
|
||||
A1 e2@--> C1 & D1
|
||||
`);
|
||||
|
||||
const data4Layout = flow.parser.yy.getData();
|
||||
expect(data4Layout.nodes.length).toBe(7);
|
||||
expect(data4Layout.edges.length).toBe(6);
|
||||
expect(data4Layout.edges[0].id).toEqual('L_A_C_0');
|
||||
expect(data4Layout.edges[1].id).toEqual('L_A_D_0');
|
||||
expect(data4Layout.edges[2].id).toEqual('e1');
|
||||
expect(data4Layout.edges[3].id).toEqual('L_B_D_0');
|
||||
expect(data4Layout.edges[4].id).toEqual('e2');
|
||||
expect(data4Layout.edges[5].id).toEqual('L_A1_D1_0');
|
||||
});
|
||||
|
||||
it('should handle redefine same edge ids again', function () {
|
||||
const res = flow.parser.parse(`flowchart TD
|
||||
A & B e1@--> C & D
|
||||
A1 e1@--> C1 & D1
|
||||
`);
|
||||
|
||||
const data4Layout = flow.parser.yy.getData();
|
||||
expect(data4Layout.nodes.length).toBe(7);
|
||||
expect(data4Layout.edges.length).toBe(6);
|
||||
expect(data4Layout.edges[0].id).toEqual('L_A_C_0');
|
||||
expect(data4Layout.edges[1].id).toEqual('L_A_D_0');
|
||||
expect(data4Layout.edges[2].id).toEqual('e1');
|
||||
expect(data4Layout.edges[3].id).toEqual('L_B_D_0');
|
||||
expect(data4Layout.edges[4].id).toEqual('L_A1_C1_0');
|
||||
expect(data4Layout.edges[5].id).toEqual('L_A1_D1_0');
|
||||
});
|
||||
|
||||
it('should handle overriding edge animate again', function () {
|
||||
const res = flow.parser.parse(`flowchart TD
|
||||
A e1@--> B
|
||||
C e2@--> D
|
||||
E e3@--> F
|
||||
e1@{ animate: true }
|
||||
e2@{ animate: false }
|
||||
e3@{ animate: true }
|
||||
e3@{ animate: false }
|
||||
`);
|
||||
|
||||
const data4Layout = flow.parser.yy.getData();
|
||||
expect(data4Layout.nodes.length).toBe(6);
|
||||
expect(data4Layout.edges.length).toBe(3);
|
||||
expect(data4Layout.edges[0].id).toEqual('e1');
|
||||
expect(data4Layout.edges[0].animate).toEqual(true);
|
||||
expect(data4Layout.edges[1].id).toEqual('e2');
|
||||
expect(data4Layout.edges[1].animate).toEqual(false);
|
||||
expect(data4Layout.edges[2].id).toEqual('e3');
|
||||
expect(data4Layout.edges[2].animate).toEqual(false);
|
||||
});
|
||||
|
||||
it.skip('should be possible to use @ syntax to add labels with trail spaces', function () {
|
||||
const res = flow.parser.parse(
|
||||
`flowchart TB
|
||||
n2["label for n2"] & n4@{ label: "labe for n4"} & n5@{ label: "labe for n5"} `
|
||||
);
|
||||
|
||||
const data4Layout = flow.parser.yy.getData();
|
||||
expect(data4Layout.nodes.length).toBe(3);
|
||||
expect(data4Layout.nodes[0].label).toEqual('label for n2');
|
||||
expect(data4Layout.nodes[1].label).toEqual('labe for n4');
|
||||
expect(data4Layout.nodes[2].label).toEqual('labe for n5');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import flowDb from '../flowDb.js';
|
||||
import flow from './flow.jison';
|
||||
import { FlowDB } from '../flowDb.js';
|
||||
import flow from './flowParser.ts';
|
||||
import { setConfig } from '../../../config.js';
|
||||
|
||||
setConfig({
|
||||
@@ -31,7 +31,7 @@ const specialChars = ['#', ':', '0', '&', ',', '*', '.', '\\', 'v', '-', '/', '_
|
||||
|
||||
describe('[Singlenodes] when parsing', () => {
|
||||
beforeEach(function () {
|
||||
flow.parser.yy = flowDb;
|
||||
flow.parser.yy = new FlowDB();
|
||||
flow.parser.yy.clear();
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import flowDb from '../flowDb.js';
|
||||
import flow from './flow.jison';
|
||||
import { FlowDB } from '../flowDb.js';
|
||||
import flow from './flowParser.ts';
|
||||
import { setConfig } from '../../../config.js';
|
||||
|
||||
setConfig({
|
||||
@@ -8,7 +8,7 @@ setConfig({
|
||||
|
||||
describe('[Style] when parsing', () => {
|
||||
beforeEach(function () {
|
||||
flow.parser.yy = flowDb;
|
||||
flow.parser.yy = new FlowDB();
|
||||
flow.parser.yy.clear();
|
||||
flow.parser.yy.setGen('gen-2');
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import flowDb from '../flowDb.js';
|
||||
import flow from './flow.jison';
|
||||
import { FlowDB } from '../flowDb.js';
|
||||
import flow from './flowParser.ts';
|
||||
import { setConfig } from '../../../config.js';
|
||||
|
||||
setConfig({
|
||||
@@ -8,7 +8,7 @@ setConfig({
|
||||
|
||||
describe('[Text] when parsing', () => {
|
||||
beforeEach(function () {
|
||||
flow.parser.yy = flowDb;
|
||||
flow.parser.yy = new FlowDB();
|
||||
flow.parser.yy.clear();
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import flowDb from '../flowDb.js';
|
||||
import flow from './flow.jison';
|
||||
import { FlowDB } from '../flowDb.js';
|
||||
import flow from './flowParser.ts';
|
||||
import { setConfig } from '../../../config.js';
|
||||
|
||||
setConfig({
|
||||
@@ -8,7 +8,7 @@ setConfig({
|
||||
|
||||
describe('when parsing flowcharts', function () {
|
||||
beforeEach(function () {
|
||||
flow.parser.yy = flowDb;
|
||||
flow.parser.yy = new FlowDB();
|
||||
flow.parser.yy.clear();
|
||||
flow.parser.yy.setGen('gen-2');
|
||||
});
|
||||
|
||||
@@ -141,6 +141,7 @@ that id.
|
||||
.*direction\s+RL[^\n]* return 'direction_rl';
|
||||
.*direction\s+LR[^\n]* return 'direction_lr';
|
||||
|
||||
[^\s\"]+\@(?=[^\{\"]) { return 'LINK_ID'; }
|
||||
[0-9]+ return 'NUM';
|
||||
\# return 'BRKT';
|
||||
":::" return 'STYLE_SEPARATOR';
|
||||
@@ -201,7 +202,9 @@ that id.
|
||||
"*" return 'MULT';
|
||||
"#" return 'BRKT';
|
||||
"&" return 'AMP';
|
||||
([A-Za-z0-9!"\#$%&'*+\.`?\\_\/]|\-(?=[^\>\-\.])|=(?!=))+ return 'NODE_STRING';
|
||||
([A-Za-z0-9!"\#$%&'*+\.`?\\_\/]|\-(?=[^\>\-\.])|=(?!=))+ {
|
||||
return 'NODE_STRING';
|
||||
}
|
||||
"-" return 'MINUS'
|
||||
[\u00AA\u00B5\u00BA\u00C0-\u00D6\u00D8-\u00F6]|
|
||||
[\u00F8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377]|
|
||||
@@ -361,7 +364,7 @@ spaceList
|
||||
|
||||
statement
|
||||
: vertexStatement separator
|
||||
{ /* console.warn('finat vs', $vertexStatement.nodes); */ $$=$vertexStatement.nodes}
|
||||
{ $$=$vertexStatement.nodes}
|
||||
| styleStatement separator
|
||||
{$$=[];}
|
||||
| linkStyleStatement separator
|
||||
@@ -396,7 +399,7 @@ shapeData:
|
||||
;
|
||||
|
||||
vertexStatement: vertexStatement link node shapeData
|
||||
{ /* console.warn('vs shapeData',$vertexStatement.stmt,$node, $shapeData);*/ yy.addVertex($node[0],undefined,undefined,undefined, undefined,undefined, undefined,$shapeData); yy.addLink($vertexStatement.stmt,$node,$link); $$ = { stmt: $node, nodes: $node.concat($vertexStatement.nodes) } }
|
||||
{ /* console.warn('vs shapeData',$vertexStatement.stmt,$node, $shapeData);*/ yy.addVertex($node[$node.length-1],undefined,undefined,undefined, undefined,undefined, undefined,$shapeData); yy.addLink($vertexStatement.stmt,$node,$link); $$ = { stmt: $node, nodes: $node.concat($vertexStatement.nodes) } }
|
||||
| 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
|
||||
@@ -404,7 +407,7 @@ vertexStatement: vertexStatement link node shapeData
|
||||
|node spaceList { /*console.warn('vertexStatement: node spaceList', $node);*/ $$ = {stmt: $node, nodes:$node }}
|
||||
|node shapeData {
|
||||
/*console.warn('vertexStatement: node shapeData', $node[0], $shapeData);*/
|
||||
yy.addVertex($node[0],undefined,undefined,undefined, undefined,undefined, undefined,$shapeData);
|
||||
yy.addVertex($node[$node.length-1],undefined,undefined,undefined, undefined,undefined, undefined,$shapeData);
|
||||
$$ = {stmt: $node, nodes:$node, shapeData: $shapeData}
|
||||
}
|
||||
|node { /* console.warn('vertexStatement: single node', $node); */ $$ = {stmt: $node, nodes:$node }}
|
||||
@@ -413,7 +416,7 @@ vertexStatement: vertexStatement link node shapeData
|
||||
node: styledVertex
|
||||
{ /*console.warn('nod', $styledVertex);*/ $$ = [$styledVertex];}
|
||||
| node shapeData spaceList AMP spaceList styledVertex
|
||||
{ yy.addVertex($node[0],undefined,undefined,undefined, undefined,undefined, undefined,$shapeData); $$ = $node.concat($styledVertex); /*console.warn('pip2', $node[0], $styledVertex, $$);*/ }
|
||||
{ yy.addVertex($node[$node.length-1],undefined,undefined,undefined, undefined,undefined, undefined,$shapeData); $$ = $node.concat($styledVertex); /*console.warn('pip2', $node[0], $styledVertex, $$);*/ }
|
||||
| node spaceList AMP spaceList styledVertex
|
||||
{ $$ = $node.concat($styledVertex); /*console.warn('pip', $node[0], $styledVertex, $$);*/ }
|
||||
;
|
||||
@@ -472,6 +475,8 @@ link: linkStatement arrowText
|
||||
{$$ = $linkStatement;}
|
||||
| START_LINK edgeText LINK
|
||||
{var inf = yy.destructLink($LINK, $START_LINK); $$ = {"type":inf.type,"stroke":inf.stroke,"length":inf.length,"text":$edgeText};}
|
||||
| LINK_ID START_LINK edgeText LINK
|
||||
{var inf = yy.destructLink($LINK, $START_LINK); $$ = {"type":inf.type,"stroke":inf.stroke,"length":inf.length,"text":$edgeText, "id": $LINK_ID};}
|
||||
;
|
||||
|
||||
edgeText: edgeTextToken
|
||||
@@ -487,6 +492,8 @@ edgeText: edgeTextToken
|
||||
|
||||
linkStatement: LINK
|
||||
{var inf = yy.destructLink($LINK);$$ = {"type":inf.type,"stroke":inf.stroke,"length":inf.length};}
|
||||
| LINK_ID LINK
|
||||
{var inf = yy.destructLink($LINK);$$ = {"type":inf.type,"stroke":inf.stroke,"length":inf.length, "id": $LINK_ID};}
|
||||
;
|
||||
|
||||
arrowText:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import flowDb from '../flowDb.js';
|
||||
import flow from './flow.jison';
|
||||
import { FlowDB } from '../flowDb.js';
|
||||
import flow from './flowParser.ts';
|
||||
import { cleanupComments } from '../../../diagram-api/comments.js';
|
||||
import { setConfig } from '../../../config.js';
|
||||
|
||||
@@ -9,7 +9,7 @@ setConfig({
|
||||
|
||||
describe('parsing a flow chart', function () {
|
||||
beforeEach(function () {
|
||||
flow.parser.yy = flowDb;
|
||||
flow.parser.yy = new FlowDB();
|
||||
flow.parser.yy.clear();
|
||||
});
|
||||
|
||||
|
||||
12
packages/mermaid/src/diagrams/flowchart/parser/flowParser.ts
Normal file
12
packages/mermaid/src/diagrams/flowchart/parser/flowParser.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
// @ts-ignore: JISON doesn't support types
|
||||
import flowJisonParser from './flow.jison';
|
||||
|
||||
const newParser = Object.assign({}, flowJisonParser);
|
||||
|
||||
newParser.parse = (src: string): unknown => {
|
||||
// remove the trailing whitespace after closing curly braces when ending a line break
|
||||
const newSrc = src.replace(/}\s*\n/g, '}\n');
|
||||
return flowJisonParser.parse(newSrc);
|
||||
};
|
||||
|
||||
export default newParser;
|
||||
@@ -1,5 +1,5 @@
|
||||
import flowDb from '../flowDb.js';
|
||||
import flow from './flow.jison';
|
||||
import { FlowDB } from '../flowDb.js';
|
||||
import flow from './flowParser.ts';
|
||||
import { setConfig } from '../../../config.js';
|
||||
|
||||
setConfig({
|
||||
@@ -8,7 +8,7 @@ setConfig({
|
||||
|
||||
describe('when parsing subgraphs', function () {
|
||||
beforeEach(function () {
|
||||
flow.parser.yy = flowDb;
|
||||
flow.parser.yy = new FlowDB();
|
||||
flow.parser.yy.clear();
|
||||
flow.parser.yy.setGen('gen-2');
|
||||
});
|
||||
|
||||
@@ -53,6 +53,7 @@ export interface FlowText {
|
||||
}
|
||||
|
||||
export interface FlowEdge {
|
||||
isUserDefinedId: boolean;
|
||||
start: string;
|
||||
end: string;
|
||||
interpolate?: string;
|
||||
@@ -62,6 +63,10 @@ export interface FlowEdge {
|
||||
length?: number;
|
||||
text: string;
|
||||
labelType: 'text';
|
||||
classes: string[];
|
||||
id?: string;
|
||||
animation?: 'fast' | 'slow';
|
||||
animate?: boolean;
|
||||
}
|
||||
|
||||
export interface FlowClass {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const getStyles = (options) =>
|
||||
`
|
||||
.mermaid-main-font {
|
||||
font-family: var(--mermaid-font-family, "trebuchet ms", verdana, arial, sans-serif);
|
||||
font-family: ${options.fontFamily};
|
||||
}
|
||||
|
||||
.exclude-range {
|
||||
@@ -45,7 +45,7 @@ const getStyles = (options) =>
|
||||
|
||||
.sectionTitle {
|
||||
text-anchor: start;
|
||||
font-family: var(--mermaid-font-family, "trebuchet ms", verdana, arial, sans-serif);
|
||||
font-family: ${options.fontFamily};
|
||||
}
|
||||
|
||||
|
||||
@@ -86,13 +86,13 @@ const getStyles = (options) =>
|
||||
|
||||
.taskText {
|
||||
text-anchor: middle;
|
||||
font-family: var(--mermaid-font-family, "trebuchet ms", verdana, arial, sans-serif);
|
||||
font-family: ${options.fontFamily};
|
||||
}
|
||||
|
||||
.taskTextOutsideRight {
|
||||
fill: ${options.taskTextDarkColor};
|
||||
text-anchor: start;
|
||||
font-family: var(--mermaid-font-family, "trebuchet ms", verdana, arial, sans-serif);
|
||||
font-family: ${options.fontFamily};
|
||||
}
|
||||
|
||||
.taskTextOutsideLeft {
|
||||
@@ -248,7 +248,7 @@ const getStyles = (options) =>
|
||||
text-anchor: middle;
|
||||
font-size: 18px;
|
||||
fill: ${options.titleColor || options.textColor};
|
||||
font-family: var(--mermaid-font-family, "trebuchet ms", verdana, arial, sans-serif);
|
||||
font-family: ${options.fontFamily};
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -84,7 +84,7 @@ describe('when parsing a gitGraph', function () {
|
||||
const commits = db.getCommits();
|
||||
|
||||
expect(commits.size).toBe(1);
|
||||
const key = commits.keys().next().value;
|
||||
const key = commits.keys().next().value!;
|
||||
expect(commits.get(key)?.message).toBe('a commit');
|
||||
expect(db.getCurrentBranch()).toBe('main');
|
||||
});
|
||||
@@ -246,7 +246,7 @@ describe('when parsing a gitGraph', function () {
|
||||
expect(db.getCurrentBranch()).toBe('main');
|
||||
expect(db.getDirection()).toBe('LR');
|
||||
expect(db.getBranches().size).toBe(1);
|
||||
const key = commits.keys().next().value;
|
||||
const key = commits.keys().next().value!;
|
||||
expect(commits.get(key)?.message).toBe('');
|
||||
expect(commits.get(key)?.id).not.toBeNull();
|
||||
expect(commits.get(key)?.tags).toStrictEqual([]);
|
||||
@@ -263,7 +263,7 @@ describe('when parsing a gitGraph', function () {
|
||||
expect(db.getCurrentBranch()).toBe('main');
|
||||
expect(db.getDirection()).toBe('LR');
|
||||
expect(db.getBranches().size).toBe(1);
|
||||
const key = commits.keys().next().value;
|
||||
const key = commits.keys().next().value!;
|
||||
expect(commits.get(key)?.message).toBe('');
|
||||
expect(commits.get(key)?.id).toBe('1111');
|
||||
expect(commits.get(key)?.tags).toStrictEqual([]);
|
||||
@@ -281,7 +281,7 @@ describe('when parsing a gitGraph', function () {
|
||||
expect(db.getCurrentBranch()).toBe('main');
|
||||
expect(db.getDirection()).toBe('LR');
|
||||
expect(db.getBranches().size).toBe(1);
|
||||
const key = commits.keys().next().value;
|
||||
const key = commits.keys().next().value!;
|
||||
expect(commits.get(key)?.message).toBe('');
|
||||
expect(commits.get(key)?.id).not.toBeNull();
|
||||
expect(commits.get(key)?.tags).toStrictEqual(['test']);
|
||||
@@ -299,7 +299,7 @@ describe('when parsing a gitGraph', function () {
|
||||
expect(db.getCurrentBranch()).toBe('main');
|
||||
expect(db.getDirection()).toBe('LR');
|
||||
expect(db.getBranches().size).toBe(1);
|
||||
const key = commits.keys().next().value;
|
||||
const key = commits.keys().next().value!;
|
||||
expect(commits.get(key)?.message).toBe('');
|
||||
expect(commits.get(key)?.id).not.toBeNull();
|
||||
expect(commits.get(key)?.tags).toStrictEqual([]);
|
||||
@@ -317,7 +317,7 @@ describe('when parsing a gitGraph', function () {
|
||||
expect(db.getCurrentBranch()).toBe('main');
|
||||
expect(db.getDirection()).toBe('LR');
|
||||
expect(db.getBranches().size).toBe(1);
|
||||
const key = commits.keys().next().value;
|
||||
const key = commits.keys().next().value!;
|
||||
expect(commits.get(key)?.message).toBe('');
|
||||
expect(commits.get(key)?.id).not.toBeNull();
|
||||
expect(commits.get(key)?.tags).toStrictEqual([]);
|
||||
@@ -335,7 +335,7 @@ describe('when parsing a gitGraph', function () {
|
||||
expect(db.getCurrentBranch()).toBe('main');
|
||||
expect(db.getDirection()).toBe('LR');
|
||||
expect(db.getBranches().size).toBe(1);
|
||||
const key = commits.keys().next().value;
|
||||
const key = commits.keys().next().value!;
|
||||
expect(commits.get(key)?.message).toBe('');
|
||||
expect(commits.get(key)?.id).not.toBeNull();
|
||||
expect(commits.get(key)?.tags).toStrictEqual([]);
|
||||
@@ -353,7 +353,7 @@ describe('when parsing a gitGraph', function () {
|
||||
expect(db.getCurrentBranch()).toBe('main');
|
||||
expect(db.getDirection()).toBe('LR');
|
||||
expect(db.getBranches().size).toBe(1);
|
||||
const key = commits.keys().next().value;
|
||||
const key = commits.keys().next().value!;
|
||||
expect(commits.get(key)?.message).toBe('test commit');
|
||||
expect(commits.get(key)?.id).not.toBeNull();
|
||||
expect(commits.get(key)?.tags).toStrictEqual([]);
|
||||
@@ -371,7 +371,7 @@ describe('when parsing a gitGraph', function () {
|
||||
expect(db.getCurrentBranch()).toBe('main');
|
||||
expect(db.getDirection()).toBe('LR');
|
||||
expect(db.getBranches().size).toBe(1);
|
||||
const key = commits.keys().next().value;
|
||||
const key = commits.keys().next().value!;
|
||||
expect(commits.get(key)?.message).toBe('test commit');
|
||||
expect(commits.get(key)?.id).not.toBeNull();
|
||||
expect(commits.get(key)?.tags).toStrictEqual([]);
|
||||
@@ -389,7 +389,7 @@ describe('when parsing a gitGraph', function () {
|
||||
expect(db.getCurrentBranch()).toBe('main');
|
||||
expect(db.getDirection()).toBe('LR');
|
||||
expect(db.getBranches().size).toBe(1);
|
||||
const key = commits.keys().next().value;
|
||||
const key = commits.keys().next().value!;
|
||||
expect(commits.get(key)?.message).toBe('');
|
||||
expect(commits.get(key)?.id).toBe('1111');
|
||||
expect(commits.get(key)?.tags).toStrictEqual(['test tag']);
|
||||
@@ -407,7 +407,7 @@ describe('when parsing a gitGraph', function () {
|
||||
expect(db.getCurrentBranch()).toBe('main');
|
||||
expect(db.getDirection()).toBe('LR');
|
||||
expect(db.getBranches().size).toBe(1);
|
||||
const key = commits.keys().next().value;
|
||||
const key = commits.keys().next().value!;
|
||||
expect(commits.get(key)?.message).toBe('');
|
||||
expect(commits.get(key)?.id).not.toBeNull();
|
||||
expect(commits.get(key)?.tags).toStrictEqual(['test tag']);
|
||||
@@ -425,7 +425,7 @@ describe('when parsing a gitGraph', function () {
|
||||
expect(db.getCurrentBranch()).toBe('main');
|
||||
expect(db.getDirection()).toBe('LR');
|
||||
expect(db.getBranches().size).toBe(1);
|
||||
const key = commits.keys().next().value;
|
||||
const key = commits.keys().next().value!;
|
||||
expect(commits.get(key)?.message).toBe('');
|
||||
expect(commits.get(key)?.id).not.toBeNull();
|
||||
expect(commits.get(key)?.tags).toStrictEqual(['test tag']);
|
||||
@@ -443,7 +443,7 @@ describe('when parsing a gitGraph', function () {
|
||||
expect(db.getCurrentBranch()).toBe('main');
|
||||
expect(db.getDirection()).toBe('LR');
|
||||
expect(db.getBranches().size).toBe(1);
|
||||
const key = commits.keys().next().value;
|
||||
const key = commits.keys().next().value!;
|
||||
expect(commits.get(key)?.message).toBe('');
|
||||
expect(commits.get(key)?.id).toBe('1111');
|
||||
expect(commits.get(key)?.tags).toStrictEqual(['test tag']);
|
||||
@@ -461,7 +461,7 @@ describe('when parsing a gitGraph', function () {
|
||||
expect(db.getCurrentBranch()).toBe('main');
|
||||
expect(db.getDirection()).toBe('LR');
|
||||
expect(db.getBranches().size).toBe(1);
|
||||
const key = commits.keys().next().value;
|
||||
const key = commits.keys().next().value!;
|
||||
expect(commits.get(key)?.message).toBe('test msg');
|
||||
expect(commits.get(key)?.id).toBe('1111');
|
||||
expect(commits.get(key)?.tags).toStrictEqual(['test tag']);
|
||||
@@ -480,7 +480,7 @@ describe('when parsing a gitGraph', function () {
|
||||
expect(db.getCurrentBranch()).toBe('main');
|
||||
expect(db.getDirection()).toBe('LR');
|
||||
expect(db.getBranches().size).toBe(1);
|
||||
const key = commits.keys().next().value;
|
||||
const key = commits.keys().next().value!;
|
||||
expect(commits.get(key)?.message).toBe('test msg');
|
||||
expect(commits.get(key)?.id).toBe('1111');
|
||||
expect(commits.get(key)?.tags).toStrictEqual(['test tag']);
|
||||
@@ -498,7 +498,7 @@ describe('when parsing a gitGraph', function () {
|
||||
expect(db.getCurrentBranch()).toBe('main');
|
||||
expect(db.getDirection()).toBe('LR');
|
||||
expect(db.getBranches().size).toBe(1);
|
||||
const key = commits.keys().next().value;
|
||||
const key = commits.keys().next().value!;
|
||||
expect(commits.get(key)?.message).toBe('test msg');
|
||||
expect(commits.get(key)?.id).toBe('1111');
|
||||
expect(commits.get(key)?.tags).toStrictEqual(['test tag']);
|
||||
@@ -516,7 +516,7 @@ describe('when parsing a gitGraph', function () {
|
||||
expect(db.getCurrentBranch()).toBe('main');
|
||||
expect(db.getDirection()).toBe('LR');
|
||||
expect(db.getBranches().size).toBe(1);
|
||||
const key = commits.keys().next().value;
|
||||
const key = commits.keys().next().value!;
|
||||
expect(commits.get(key)?.message).toBe('test msg');
|
||||
expect(commits.get(key)?.id).toBe('1111');
|
||||
expect(commits.get(key)?.tags).toStrictEqual(['test tag']);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { InfoFields, InfoDB } from './infoTypes.js';
|
||||
import { version } from '../../../package.json';
|
||||
import packageJson from '../../../package.json' assert { type: 'json' };
|
||||
|
||||
export const DEFAULT_INFO_DB: InfoFields = { version } as const;
|
||||
export const DEFAULT_INFO_DB: InfoFields = { version: packageJson.version } as const;
|
||||
|
||||
export const getVersion = (): string => DEFAULT_INFO_DB.version;
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ const parserFnConstructor = (str: string) => {
|
||||
};
|
||||
};
|
||||
|
||||
const mockDB: Record<string, Mock<any, any>> = {
|
||||
const mockDB: Record<string, Mock<any>> = {
|
||||
setQuadrant1Text: vi.fn(),
|
||||
setQuadrant2Text: vi.fn(),
|
||||
setQuadrant3Text: vi.fn(),
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
%x string
|
||||
%x token
|
||||
%x unqString
|
||||
%x style
|
||||
%x acc_title
|
||||
%x acc_descr
|
||||
%x acc_descr_multiline
|
||||
@@ -22,6 +23,10 @@ accDescr\s*":"\s* { this.begin("ac
|
||||
accDescr\s*"{"\s* { this.begin("acc_descr_multiline");}
|
||||
<acc_descr_multiline>[\}] { this.popState(); }
|
||||
<acc_descr_multiline>[^\}]* return "acc_descr_multiline_value";
|
||||
.*direction\s+TB[^\n]* return 'direction_tb';
|
||||
.*direction\s+BT[^\n]* return 'direction_bt';
|
||||
.*direction\s+RL[^\n]* return 'direction_rl';
|
||||
.*direction\s+LR[^\n]* return 'direction_lr';
|
||||
(\r?\n)+ return 'NEWLINE';
|
||||
\s+ /* skip all whitespace */
|
||||
\#[^\n]* /* skip comments */
|
||||
@@ -32,6 +37,7 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multili
|
||||
|
||||
"{" return 'STRUCT_START';
|
||||
"}" return 'STRUCT_STOP';
|
||||
":"{3} return 'STYLE_SEPARATOR';
|
||||
":" return 'COLONSEP';
|
||||
|
||||
"id" return 'ID';
|
||||
@@ -68,6 +74,20 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multili
|
||||
"type" return 'TYPE';
|
||||
"docref" return 'DOCREF';
|
||||
|
||||
"style" { this.begin("style"); return 'STYLE'; }
|
||||
<style>\w+ return 'ALPHA';
|
||||
<style>":" return 'COLON';
|
||||
<style>";" return 'SEMICOLON';
|
||||
<style>"%" return 'PERCENT';
|
||||
<style>"-" return 'MINUS';
|
||||
<style>"#" return 'BRKT';
|
||||
<style>" " /* skip spaces */
|
||||
<style>["] { this.begin("string"); }
|
||||
<style>\n { this.popState(); }
|
||||
|
||||
"classDef" { this.begin("style"); return 'CLASSDEF'; }
|
||||
"class" { this.begin("style"); return 'CLASS'; }
|
||||
|
||||
"<-" return 'END_ARROW_L';
|
||||
"->" {return 'END_ARROW_R';}
|
||||
"-" {return 'LINE';}
|
||||
@@ -76,7 +96,11 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multili
|
||||
<string>["] { this.popState(); }
|
||||
<string>[^"]* { return "qString"; }
|
||||
|
||||
[\w][^\r\n\{\<\>\-\=]* { yytext = yytext.trim(); return 'unqString';}
|
||||
[\w][^:,\r\n\{\<\>\-\=]* { yytext = yytext.trim(); return 'unqString';}
|
||||
|
||||
<*>\w+ return 'ALPHA';
|
||||
<*>[0-9]+ return 'NUM';
|
||||
<*>"," return 'COMMA';
|
||||
|
||||
/lex
|
||||
|
||||
@@ -101,11 +125,28 @@ diagram
|
||||
| elementDef diagram
|
||||
| relationshipDef diagram
|
||||
| directive diagram
|
||||
| NEWLINE diagram;
|
||||
| direction diagram
|
||||
| styleStatement diagram
|
||||
| classDefStatement diagram
|
||||
| classStatement diagram
|
||||
| NEWLINE diagram
|
||||
;
|
||||
|
||||
direction
|
||||
: direction_tb
|
||||
{ yy.setDirection('TB');}
|
||||
| direction_bt
|
||||
{ yy.setDirection('BT');}
|
||||
| direction_rl
|
||||
{ yy.setDirection('RL');}
|
||||
| direction_lr
|
||||
{ yy.setDirection('LR');}
|
||||
;
|
||||
|
||||
requirementDef
|
||||
: requirementType requirementName STRUCT_START NEWLINE requirementBody
|
||||
{ yy.addRequirement($2, $1) };
|
||||
: requirementType requirementName STRUCT_START NEWLINE requirementBody { yy.addRequirement($2, $1) }
|
||||
| requirementType requirementName STYLE_SEPARATOR idList STRUCT_START NEWLINE requirementBody { yy.addRequirement($2, $1); yy.setClass([$2], $4); }
|
||||
;
|
||||
|
||||
requirementBody
|
||||
: ID COLONSEP id NEWLINE requirementBody
|
||||
@@ -149,8 +190,9 @@ verifyType
|
||||
{ $$=yy.VerifyType.VERIFY_TEST;};
|
||||
|
||||
elementDef
|
||||
: ELEMENT elementName STRUCT_START NEWLINE elementBody
|
||||
{ yy.addElement($2) };
|
||||
: ELEMENT elementName STRUCT_START NEWLINE elementBody { yy.addElement($2) }
|
||||
| ELEMENT elementName STYLE_SEPARATOR idList STRUCT_START NEWLINE elementBody { yy.addElement($2); yy.setClass([$2], $4); }
|
||||
;
|
||||
|
||||
elementBody
|
||||
: TYPE COLONSEP type NEWLINE elementBody
|
||||
@@ -182,6 +224,38 @@ relationship
|
||||
| TRACES
|
||||
{ $$=yy.Relationships.TRACES;};
|
||||
|
||||
classDefStatement
|
||||
: CLASSDEF idList stylesOpt {$$ = $CLASSDEF;yy.defineClass($idList,$stylesOpt);}
|
||||
;
|
||||
|
||||
classStatement
|
||||
: CLASS idList idList {yy.setClass($2, $3);}
|
||||
| id STYLE_SEPARATOR idList {yy.setClass([$1], $3);}
|
||||
;
|
||||
|
||||
idList
|
||||
: ALPHA { $$ = [$ALPHA]; }
|
||||
| idList COMMA ALPHA = { $$ = $idList.concat([$ALPHA]); }
|
||||
| id { $$ = [$id]; }
|
||||
| idList COMMA id = { $$ = $idList.concat([$id]); }
|
||||
;
|
||||
|
||||
styleStatement
|
||||
: STYLE idList stylesOpt {$$ = $STYLE;yy.setCssStyle($2,$stylesOpt);}
|
||||
;
|
||||
|
||||
stylesOpt
|
||||
: style {$$ = [$style]}
|
||||
| stylesOpt COMMA style {$stylesOpt.push($style);$$ = $stylesOpt;}
|
||||
;
|
||||
|
||||
style
|
||||
: styleComponent
|
||||
| style styleComponent {$$ = $style + $styleComponent;}
|
||||
;
|
||||
|
||||
styleComponent: ALPHA | NUM | COLON | UNIT | SPACE | BRKT | PCT | MINUS | LABEL | SEMICOLON;
|
||||
|
||||
|
||||
requirementName: unqString | qString;
|
||||
id : unqString | qString;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { setConfig } from '../../../config.js';
|
||||
import requirementDb from '../requirementDb.js';
|
||||
import { RequirementDB } from '../requirementDb.js';
|
||||
import reqDiagram from './requirementDiagram.jison';
|
||||
|
||||
setConfig({
|
||||
@@ -7,6 +7,7 @@ setConfig({
|
||||
});
|
||||
|
||||
describe('when parsing requirement diagram it...', function () {
|
||||
const requirementDb = new RequirementDB();
|
||||
beforeEach(function () {
|
||||
reqDiagram.parser.yy = requirementDb;
|
||||
reqDiagram.parser.yy.clear();
|
||||
@@ -37,7 +38,7 @@ describe('when parsing requirement diagram it...', function () {
|
||||
|
||||
let foundReq = requirementDb.getRequirements().get(expectedName);
|
||||
expect(foundReq).toBeDefined();
|
||||
expect(foundReq.id).toBe(expectedId);
|
||||
expect(foundReq.requirementId).toBe(expectedId);
|
||||
expect(foundReq.text).toBe(expectedText);
|
||||
|
||||
expect(requirementDb.getElements().size).toBe(0);
|
||||
@@ -599,4 +600,251 @@ line 2`;
|
||||
expect(reqDiagram.parser.yy.getElements().size).toBe(1);
|
||||
});
|
||||
}
|
||||
|
||||
it('will accept styling a requirement', function () {
|
||||
const expectedName = 'test_req';
|
||||
|
||||
let lines = [
|
||||
`requirementDiagram`,
|
||||
``,
|
||||
`requirement ${expectedName} {`,
|
||||
`}`,
|
||||
`style ${expectedName} fill:#f9f,stroke:#333,stroke-width:4px`,
|
||||
``,
|
||||
];
|
||||
let doc = lines.join('\n');
|
||||
|
||||
reqDiagram.parser.parse(doc);
|
||||
|
||||
let foundReq = requirementDb.getRequirements().get(expectedName);
|
||||
const styles = foundReq.cssStyles;
|
||||
expect(styles).toEqual(['fill:#f9f', 'stroke:#333', 'stroke-width:4px']);
|
||||
});
|
||||
|
||||
it('will accept styling an element', function () {
|
||||
const expectedName = 'test_element';
|
||||
|
||||
let lines = [
|
||||
`requirementDiagram`,
|
||||
``,
|
||||
`element ${expectedName} {`,
|
||||
`}`,
|
||||
`style ${expectedName} fill:#f9f,stroke:#333,stroke-width:4px`,
|
||||
``,
|
||||
];
|
||||
let doc = lines.join('\n');
|
||||
|
||||
reqDiagram.parser.parse(doc);
|
||||
|
||||
let foundElement = requirementDb.getElements().get(expectedName);
|
||||
const styles = foundElement.cssStyles;
|
||||
expect(styles).toEqual(['fill:#f9f', 'stroke:#333', 'stroke-width:4px']);
|
||||
});
|
||||
|
||||
it('will accept styling multiple things at once', function () {
|
||||
const expectedRequirementName = 'test_requirement';
|
||||
const expectedElementName = 'test_element';
|
||||
|
||||
let lines = [
|
||||
`requirementDiagram`,
|
||||
``,
|
||||
`requirement ${expectedRequirementName} {`,
|
||||
`}`,
|
||||
`element ${expectedElementName} {`,
|
||||
`}`,
|
||||
`style ${expectedRequirementName},${expectedElementName} fill:#f9f,stroke:#333,stroke-width:4px`,
|
||||
``,
|
||||
];
|
||||
let doc = lines.join('\n');
|
||||
|
||||
reqDiagram.parser.parse(doc);
|
||||
|
||||
let foundRequirement = requirementDb.getRequirements().get(expectedRequirementName);
|
||||
const requirementStyles = foundRequirement.cssStyles;
|
||||
expect(requirementStyles).toEqual(['fill:#f9f', 'stroke:#333', 'stroke-width:4px']);
|
||||
let foundElement = requirementDb.getElements().get(expectedElementName);
|
||||
const elementStyles = foundElement.cssStyles;
|
||||
expect(elementStyles).toEqual(['fill:#f9f', 'stroke:#333', 'stroke-width:4px']);
|
||||
});
|
||||
|
||||
it('will accept defining a class', function () {
|
||||
const expectedName = 'myClass';
|
||||
|
||||
let lines = [
|
||||
`requirementDiagram`,
|
||||
``,
|
||||
`classDef ${expectedName} fill:#f9f,stroke:#333,stroke-width:4px`,
|
||||
``,
|
||||
];
|
||||
let doc = lines.join('\n');
|
||||
|
||||
reqDiagram.parser.parse(doc);
|
||||
|
||||
let foundClass = requirementDb.getClasses().get(expectedName);
|
||||
expect(foundClass).toEqual({
|
||||
id: 'myClass',
|
||||
styles: ['fill:#f9f', 'stroke:#333', 'stroke-width:4px'],
|
||||
textStyles: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('will accept defining multiple classes at once', function () {
|
||||
const firstName = 'firstClass';
|
||||
const secondName = 'secondClass';
|
||||
|
||||
let lines = [
|
||||
`requirementDiagram`,
|
||||
``,
|
||||
`classDef ${firstName},${secondName} fill:#f9f,stroke:#333,stroke-width:4px`,
|
||||
``,
|
||||
];
|
||||
let doc = lines.join('\n');
|
||||
|
||||
reqDiagram.parser.parse(doc);
|
||||
|
||||
let firstClass = requirementDb.getClasses().get(firstName);
|
||||
expect(firstClass).toEqual({
|
||||
id: 'firstClass',
|
||||
styles: ['fill:#f9f', 'stroke:#333', 'stroke-width:4px'],
|
||||
textStyles: [],
|
||||
});
|
||||
let secondClass = requirementDb.getClasses().get(secondName);
|
||||
expect(secondClass).toEqual({
|
||||
id: 'secondClass',
|
||||
styles: ['fill:#f9f', 'stroke:#333', 'stroke-width:4px'],
|
||||
textStyles: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('will accept assigning a class via the class statement', function () {
|
||||
const requirementName = 'myReq';
|
||||
const className = 'myClass';
|
||||
|
||||
let lines = [
|
||||
`requirementDiagram`,
|
||||
``,
|
||||
`requirement ${requirementName} {`,
|
||||
`}`,
|
||||
`classDef ${className} fill:#f9f,stroke:#333,stroke-width:4px`,
|
||||
`class ${requirementName} ${className}`,
|
||||
``,
|
||||
];
|
||||
let doc = lines.join('\n');
|
||||
|
||||
reqDiagram.parser.parse(doc);
|
||||
|
||||
let foundRequirement = requirementDb.getRequirements().get(requirementName);
|
||||
expect(foundRequirement.classes).toEqual(['default', className]);
|
||||
});
|
||||
|
||||
it('will accept assigning multiple classes to multiple things via the class statement', function () {
|
||||
const requirementName = 'req';
|
||||
const elementName = 'elem';
|
||||
const firstClassName = 'class1';
|
||||
const secondClassName = 'class2';
|
||||
|
||||
let lines = [
|
||||
`requirementDiagram`,
|
||||
``,
|
||||
`requirement ${requirementName} {`,
|
||||
`}`,
|
||||
`element ${elementName} {`,
|
||||
`}`,
|
||||
`classDef ${firstClassName},${secondClassName} fill:#f9f,stroke:#333,stroke-width:4px`,
|
||||
`class ${requirementName},${elementName} ${firstClassName},${secondClassName}`,
|
||||
``,
|
||||
];
|
||||
let doc = lines.join('\n');
|
||||
|
||||
reqDiagram.parser.parse(doc);
|
||||
|
||||
let requirement = requirementDb.getRequirements().get(requirementName);
|
||||
expect(requirement.classes).toEqual(['default', firstClassName, secondClassName]);
|
||||
let element = requirementDb.getElements().get(elementName);
|
||||
expect(element.classes).toEqual(['default', firstClassName, secondClassName]);
|
||||
});
|
||||
|
||||
it('will accept assigning a class via the shorthand syntax', function () {
|
||||
const requirementName = 'myReq';
|
||||
const className = 'myClass';
|
||||
|
||||
let lines = [
|
||||
`requirementDiagram`,
|
||||
``,
|
||||
`requirement ${requirementName} {`,
|
||||
`}`,
|
||||
`classDef ${className} fill:#f9f,stroke:#333,stroke-width:4px`,
|
||||
`${requirementName}:::${className}`,
|
||||
``,
|
||||
];
|
||||
let doc = lines.join('\n');
|
||||
|
||||
reqDiagram.parser.parse(doc);
|
||||
|
||||
let foundRequirement = requirementDb.getRequirements().get(requirementName);
|
||||
expect(foundRequirement.classes).toEqual(['default', className]);
|
||||
});
|
||||
|
||||
it('will accept assigning multiple classes via the shorthand syntax', function () {
|
||||
const requirementName = 'myReq';
|
||||
const firstClassName = 'class1';
|
||||
const secondClassName = 'class2';
|
||||
|
||||
let lines = [
|
||||
`requirementDiagram`,
|
||||
``,
|
||||
`requirement ${requirementName} {`,
|
||||
`}`,
|
||||
`classDef ${firstClassName} fill:#f9f,stroke:#333,stroke-width:4px`,
|
||||
`classDef ${secondClassName} color:blue`,
|
||||
`${requirementName}:::${firstClassName},${secondClassName}`,
|
||||
``,
|
||||
];
|
||||
let doc = lines.join('\n');
|
||||
|
||||
reqDiagram.parser.parse(doc);
|
||||
|
||||
let foundRequirement = requirementDb.getRequirements().get(requirementName);
|
||||
expect(foundRequirement.classes).toEqual(['default', firstClassName, secondClassName]);
|
||||
});
|
||||
|
||||
it('will accept assigning a class or multiple via the shorthand syntax when defining a requirement or element', function () {
|
||||
const requirementName = 'myReq';
|
||||
const elementName = 'myElem';
|
||||
const firstClassName = 'class1';
|
||||
const secondClassName = 'class2';
|
||||
|
||||
let lines = [
|
||||
`requirementDiagram`,
|
||||
``,
|
||||
`requirement ${requirementName}:::${firstClassName} {`,
|
||||
`}`,
|
||||
`element ${elementName}:::${firstClassName},${secondClassName} {`,
|
||||
`}`,
|
||||
``,
|
||||
`classDef ${firstClassName} fill:#f9f,stroke:#333,stroke-width:4px`,
|
||||
`classDef ${secondClassName} color:blue`,
|
||||
``,
|
||||
];
|
||||
let doc = lines.join('\n');
|
||||
|
||||
reqDiagram.parser.parse(doc);
|
||||
|
||||
let foundRequirement = requirementDb.getRequirements().get(requirementName);
|
||||
expect(foundRequirement.classes).toEqual(['default', firstClassName]);
|
||||
let foundElement = requirementDb.getElements().get(elementName);
|
||||
expect(foundElement.classes).toEqual(['default', firstClassName, secondClassName]);
|
||||
});
|
||||
|
||||
describe('will parse direction statements and', () => {
|
||||
test.each(['TB', 'BT', 'LR', 'RL'])('will accept direction %s', (directionVal) => {
|
||||
const lines = ['requirementDiagram', '', `direction ${directionVal}`, ''];
|
||||
const doc = lines.join('\n');
|
||||
|
||||
reqDiagram.parser.parse(doc);
|
||||
|
||||
const direction = requirementDb.getDirection();
|
||||
expect(direction).toBe(directionVal);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,168 +0,0 @@
|
||||
import { getConfig } from '../../diagram-api/diagramAPI.js';
|
||||
import { log } from '../../logger.js';
|
||||
|
||||
import {
|
||||
setAccTitle,
|
||||
getAccTitle,
|
||||
getAccDescription,
|
||||
setAccDescription,
|
||||
clear as commonClear,
|
||||
} from '../common/commonDb.js';
|
||||
|
||||
let relations = [];
|
||||
let latestRequirement = {};
|
||||
let requirements = new Map();
|
||||
let latestElement = {};
|
||||
let elements = new Map();
|
||||
|
||||
const RequirementType = {
|
||||
REQUIREMENT: 'Requirement',
|
||||
FUNCTIONAL_REQUIREMENT: 'Functional Requirement',
|
||||
INTERFACE_REQUIREMENT: 'Interface Requirement',
|
||||
PERFORMANCE_REQUIREMENT: 'Performance Requirement',
|
||||
PHYSICAL_REQUIREMENT: 'Physical Requirement',
|
||||
DESIGN_CONSTRAINT: 'Design Constraint',
|
||||
};
|
||||
|
||||
const RiskLevel = {
|
||||
LOW_RISK: 'Low',
|
||||
MED_RISK: 'Medium',
|
||||
HIGH_RISK: 'High',
|
||||
};
|
||||
|
||||
const VerifyType = {
|
||||
VERIFY_ANALYSIS: 'Analysis',
|
||||
VERIFY_DEMONSTRATION: 'Demonstration',
|
||||
VERIFY_INSPECTION: 'Inspection',
|
||||
VERIFY_TEST: 'Test',
|
||||
};
|
||||
|
||||
const Relationships = {
|
||||
CONTAINS: 'contains',
|
||||
COPIES: 'copies',
|
||||
DERIVES: 'derives',
|
||||
SATISFIES: 'satisfies',
|
||||
VERIFIES: 'verifies',
|
||||
REFINES: 'refines',
|
||||
TRACES: 'traces',
|
||||
};
|
||||
|
||||
const addRequirement = (name, type) => {
|
||||
if (!requirements.has(name)) {
|
||||
requirements.set(name, {
|
||||
name,
|
||||
type,
|
||||
|
||||
id: latestRequirement.id,
|
||||
text: latestRequirement.text,
|
||||
risk: latestRequirement.risk,
|
||||
verifyMethod: latestRequirement.verifyMethod,
|
||||
});
|
||||
}
|
||||
latestRequirement = {};
|
||||
|
||||
return requirements.get(name);
|
||||
};
|
||||
|
||||
const getRequirements = () => requirements;
|
||||
|
||||
const setNewReqId = (id) => {
|
||||
if (latestRequirement !== undefined) {
|
||||
latestRequirement.id = id;
|
||||
}
|
||||
};
|
||||
|
||||
const setNewReqText = (text) => {
|
||||
if (latestRequirement !== undefined) {
|
||||
latestRequirement.text = text;
|
||||
}
|
||||
};
|
||||
|
||||
const setNewReqRisk = (risk) => {
|
||||
if (latestRequirement !== undefined) {
|
||||
latestRequirement.risk = risk;
|
||||
}
|
||||
};
|
||||
|
||||
const setNewReqVerifyMethod = (verifyMethod) => {
|
||||
if (latestRequirement !== undefined) {
|
||||
latestRequirement.verifyMethod = verifyMethod;
|
||||
}
|
||||
};
|
||||
|
||||
const addElement = (name) => {
|
||||
if (!elements.has(name)) {
|
||||
elements.set(name, {
|
||||
name,
|
||||
type: latestElement.type,
|
||||
docRef: latestElement.docRef,
|
||||
});
|
||||
log.info('Added new requirement: ', name);
|
||||
}
|
||||
latestElement = {};
|
||||
|
||||
return elements.get(name);
|
||||
};
|
||||
|
||||
const getElements = () => elements;
|
||||
|
||||
const setNewElementType = (type) => {
|
||||
if (latestElement !== undefined) {
|
||||
latestElement.type = type;
|
||||
}
|
||||
};
|
||||
|
||||
const setNewElementDocRef = (docRef) => {
|
||||
if (latestElement !== undefined) {
|
||||
latestElement.docRef = docRef;
|
||||
}
|
||||
};
|
||||
|
||||
const addRelationship = (type, src, dst) => {
|
||||
relations.push({
|
||||
type,
|
||||
src,
|
||||
dst,
|
||||
});
|
||||
};
|
||||
|
||||
const getRelationships = () => relations;
|
||||
|
||||
const clear = () => {
|
||||
relations = [];
|
||||
latestRequirement = {};
|
||||
requirements = new Map();
|
||||
latestElement = {};
|
||||
elements = new Map();
|
||||
commonClear();
|
||||
};
|
||||
|
||||
export default {
|
||||
RequirementType,
|
||||
RiskLevel,
|
||||
VerifyType,
|
||||
Relationships,
|
||||
|
||||
getConfig: () => getConfig().req,
|
||||
|
||||
addRequirement,
|
||||
getRequirements,
|
||||
setNewReqId,
|
||||
setNewReqText,
|
||||
setNewReqRisk,
|
||||
setNewReqVerifyMethod,
|
||||
setAccTitle,
|
||||
getAccTitle,
|
||||
setAccDescription,
|
||||
getAccDescription,
|
||||
|
||||
addElement,
|
||||
getElements,
|
||||
setNewElementType,
|
||||
setNewElementDocRef,
|
||||
|
||||
addRelationship,
|
||||
getRelationships,
|
||||
|
||||
clear,
|
||||
};
|
||||
@@ -0,0 +1,96 @@
|
||||
import { RequirementDB } from './requirementDb.js';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import type { Relation, RelationshipType } from './types.js';
|
||||
|
||||
describe('requirementDb', () => {
|
||||
const requirementDb = new RequirementDB();
|
||||
beforeEach(() => {
|
||||
requirementDb.clear();
|
||||
});
|
||||
|
||||
it('should add a requirement', () => {
|
||||
requirementDb.addRequirement('requirement', 'Requirement');
|
||||
const requirements = requirementDb.getRequirements();
|
||||
expect(requirements.has('requirement')).toBe(true);
|
||||
});
|
||||
|
||||
it('should add an element', () => {
|
||||
requirementDb.addElement('element');
|
||||
const elements = requirementDb.getElements();
|
||||
expect(elements.has('element')).toBe(true);
|
||||
});
|
||||
|
||||
it('should add a relationship', () => {
|
||||
requirementDb.addRelationship('contains' as RelationshipType, 'src', 'dst');
|
||||
const relationships = requirementDb.getRelationships();
|
||||
const relationship = relationships.find(
|
||||
(r: Relation) => r.type === 'contains' && r.src === 'src' && r.dst === 'dst'
|
||||
);
|
||||
expect(relationship).toBeDefined();
|
||||
});
|
||||
|
||||
it('should detect single class', () => {
|
||||
requirementDb.defineClass(['a'], ['stroke-width: 8px']);
|
||||
const classes = requirementDb.getClasses();
|
||||
|
||||
expect(classes.has('a')).toBe(true);
|
||||
expect(classes.get('a')?.styles).toEqual(['stroke-width: 8px']);
|
||||
});
|
||||
|
||||
it('should detect many classes', () => {
|
||||
requirementDb.defineClass(['a', 'b'], ['stroke-width: 8px']);
|
||||
const classes = requirementDb.getClasses();
|
||||
|
||||
expect(classes.has('a')).toBe(true);
|
||||
expect(classes.has('b')).toBe(true);
|
||||
expect(classes.get('a')?.styles).toEqual(['stroke-width: 8px']);
|
||||
expect(classes.get('b')?.styles).toEqual(['stroke-width: 8px']);
|
||||
});
|
||||
|
||||
it('should detect direction', () => {
|
||||
requirementDb.setDirection('TB');
|
||||
const direction = requirementDb.getDirection();
|
||||
|
||||
expect(direction).toBe('TB');
|
||||
});
|
||||
|
||||
it('should add styles to a requirement and element', () => {
|
||||
requirementDb.addRequirement('requirement', 'Requirement');
|
||||
requirementDb.setCssStyle(['requirement'], ['color:red']);
|
||||
requirementDb.addElement('element');
|
||||
requirementDb.setCssStyle(['element'], ['stroke-width:4px', 'stroke: yellow']);
|
||||
|
||||
const requirement = requirementDb.getRequirements().get('requirement');
|
||||
const element = requirementDb.getElements().get('element');
|
||||
|
||||
expect(requirement?.cssStyles).toEqual(['color:red']);
|
||||
expect(element?.cssStyles).toEqual(['stroke-width:4px', 'stroke: yellow']);
|
||||
});
|
||||
|
||||
it('should add classes to a requirement and element', () => {
|
||||
requirementDb.addRequirement('requirement', 'Requirement');
|
||||
requirementDb.addElement('element');
|
||||
requirementDb.setClass(['requirement', 'element'], ['myClass']);
|
||||
|
||||
const requirement = requirementDb.getRequirements().get('requirement');
|
||||
const element = requirementDb.getElements().get('element');
|
||||
|
||||
expect(requirement?.classes).toEqual(['default', 'myClass']);
|
||||
expect(element?.classes).toEqual(['default', 'myClass']);
|
||||
});
|
||||
|
||||
it('should add styles to a requirement and element inherited from a class', () => {
|
||||
requirementDb.addRequirement('requirement', 'Requirement');
|
||||
requirementDb.addElement('element');
|
||||
requirementDb.defineClass(['myClass'], ['color:red']);
|
||||
requirementDb.defineClass(['myClass2'], ['stroke-width:4px', 'stroke: yellow']);
|
||||
requirementDb.setClass(['requirement'], ['myClass']);
|
||||
requirementDb.setClass(['element'], ['myClass2']);
|
||||
|
||||
const requirement = requirementDb.getRequirements().get('requirement');
|
||||
const element = requirementDb.getElements().get('element');
|
||||
|
||||
expect(requirement?.cssStyles).toEqual(['color:red']);
|
||||
expect(element?.cssStyles).toEqual(['stroke-width:4px', 'stroke: yellow']);
|
||||
});
|
||||
});
|
||||
349
packages/mermaid/src/diagrams/requirement/requirementDb.ts
Normal file
349
packages/mermaid/src/diagrams/requirement/requirementDb.ts
Normal file
@@ -0,0 +1,349 @@
|
||||
import { getConfig } from '../../diagram-api/diagramAPI.js';
|
||||
import type { DiagramDB } from '../../diagram-api/types.js';
|
||||
import { log } from '../../logger.js';
|
||||
import type { Node, Edge } from '../../rendering-util/types.js';
|
||||
|
||||
import {
|
||||
setAccTitle,
|
||||
getAccTitle,
|
||||
getAccDescription,
|
||||
setAccDescription,
|
||||
clear as commonClear,
|
||||
setDiagramTitle,
|
||||
getDiagramTitle,
|
||||
} from '../common/commonDb.js';
|
||||
import type {
|
||||
Element,
|
||||
Relation,
|
||||
RelationshipType,
|
||||
Requirement,
|
||||
RequirementClass,
|
||||
RequirementType,
|
||||
RiskLevel,
|
||||
VerifyType,
|
||||
} from './types.js';
|
||||
|
||||
export class RequirementDB implements DiagramDB {
|
||||
private relations: Relation[] = [];
|
||||
private latestRequirement: Requirement = this.getInitialRequirement();
|
||||
private requirements = new Map<string, Requirement>();
|
||||
private latestElement: Element = this.getInitialElement();
|
||||
private elements = new Map<string, Element>();
|
||||
private classes = new Map<string, RequirementClass>();
|
||||
private direction = 'TB';
|
||||
|
||||
private RequirementType = {
|
||||
REQUIREMENT: 'Requirement',
|
||||
FUNCTIONAL_REQUIREMENT: 'Functional Requirement',
|
||||
INTERFACE_REQUIREMENT: 'Interface Requirement',
|
||||
PERFORMANCE_REQUIREMENT: 'Performance Requirement',
|
||||
PHYSICAL_REQUIREMENT: 'Physical Requirement',
|
||||
DESIGN_CONSTRAINT: 'Design Constraint',
|
||||
};
|
||||
|
||||
private RiskLevel = {
|
||||
LOW_RISK: 'Low',
|
||||
MED_RISK: 'Medium',
|
||||
HIGH_RISK: 'High',
|
||||
};
|
||||
|
||||
private VerifyType = {
|
||||
VERIFY_ANALYSIS: 'Analysis',
|
||||
VERIFY_DEMONSTRATION: 'Demonstration',
|
||||
VERIFY_INSPECTION: 'Inspection',
|
||||
VERIFY_TEST: 'Test',
|
||||
};
|
||||
|
||||
private Relationships = {
|
||||
CONTAINS: 'contains',
|
||||
COPIES: 'copies',
|
||||
DERIVES: 'derives',
|
||||
SATISFIES: 'satisfies',
|
||||
VERIFIES: 'verifies',
|
||||
REFINES: 'refines',
|
||||
TRACES: 'traces',
|
||||
};
|
||||
|
||||
constructor() {
|
||||
this.clear();
|
||||
|
||||
// Needed for JISON since it only supports direct properties
|
||||
this.setDirection = this.setDirection.bind(this);
|
||||
this.addRequirement = this.addRequirement.bind(this);
|
||||
this.setNewReqId = this.setNewReqId.bind(this);
|
||||
this.setNewReqRisk = this.setNewReqRisk.bind(this);
|
||||
this.setNewReqText = this.setNewReqText.bind(this);
|
||||
this.setNewReqVerifyMethod = this.setNewReqVerifyMethod.bind(this);
|
||||
this.addElement = this.addElement.bind(this);
|
||||
this.setNewElementType = this.setNewElementType.bind(this);
|
||||
this.setNewElementDocRef = this.setNewElementDocRef.bind(this);
|
||||
this.addRelationship = this.addRelationship.bind(this);
|
||||
this.setCssStyle = this.setCssStyle.bind(this);
|
||||
this.setClass = this.setClass.bind(this);
|
||||
this.defineClass = this.defineClass.bind(this);
|
||||
this.setAccTitle = this.setAccTitle.bind(this);
|
||||
this.setAccDescription = this.setAccDescription.bind(this);
|
||||
}
|
||||
|
||||
public getDirection() {
|
||||
return this.direction;
|
||||
}
|
||||
public setDirection(dir: string) {
|
||||
this.direction = dir;
|
||||
}
|
||||
|
||||
private resetLatestRequirement() {
|
||||
this.latestRequirement = this.getInitialRequirement();
|
||||
}
|
||||
|
||||
private resetLatestElement() {
|
||||
this.latestElement = this.getInitialElement();
|
||||
}
|
||||
|
||||
private getInitialRequirement(): Requirement {
|
||||
return {
|
||||
requirementId: '',
|
||||
text: '',
|
||||
risk: '' as RiskLevel,
|
||||
verifyMethod: '' as VerifyType,
|
||||
name: '',
|
||||
type: '' as RequirementType,
|
||||
cssStyles: [],
|
||||
classes: ['default'],
|
||||
};
|
||||
}
|
||||
|
||||
private getInitialElement(): Element {
|
||||
return {
|
||||
name: '',
|
||||
type: '',
|
||||
docRef: '',
|
||||
cssStyles: [],
|
||||
classes: ['default'],
|
||||
};
|
||||
}
|
||||
|
||||
public addRequirement(name: string, type: RequirementType) {
|
||||
if (!this.requirements.has(name)) {
|
||||
this.requirements.set(name, {
|
||||
name,
|
||||
type,
|
||||
requirementId: this.latestRequirement.requirementId,
|
||||
text: this.latestRequirement.text,
|
||||
risk: this.latestRequirement.risk,
|
||||
verifyMethod: this.latestRequirement.verifyMethod,
|
||||
cssStyles: [],
|
||||
classes: ['default'],
|
||||
});
|
||||
}
|
||||
this.resetLatestRequirement();
|
||||
|
||||
return this.requirements.get(name);
|
||||
}
|
||||
|
||||
public getRequirements() {
|
||||
return this.requirements;
|
||||
}
|
||||
|
||||
public setNewReqId(id: string) {
|
||||
if (this.latestRequirement !== undefined) {
|
||||
this.latestRequirement.requirementId = id;
|
||||
}
|
||||
}
|
||||
|
||||
public setNewReqText(text: string) {
|
||||
if (this.latestRequirement !== undefined) {
|
||||
this.latestRequirement.text = text;
|
||||
}
|
||||
}
|
||||
|
||||
public setNewReqRisk(risk: RiskLevel) {
|
||||
if (this.latestRequirement !== undefined) {
|
||||
this.latestRequirement.risk = risk;
|
||||
}
|
||||
}
|
||||
|
||||
public setNewReqVerifyMethod(verifyMethod: VerifyType) {
|
||||
if (this.latestRequirement !== undefined) {
|
||||
this.latestRequirement.verifyMethod = verifyMethod;
|
||||
}
|
||||
}
|
||||
|
||||
public addElement(name: string) {
|
||||
if (!this.elements.has(name)) {
|
||||
this.elements.set(name, {
|
||||
name,
|
||||
type: this.latestElement.type,
|
||||
docRef: this.latestElement.docRef,
|
||||
cssStyles: [],
|
||||
classes: ['default'],
|
||||
});
|
||||
log.info('Added new element: ', name);
|
||||
}
|
||||
this.resetLatestElement();
|
||||
|
||||
return this.elements.get(name);
|
||||
}
|
||||
|
||||
public getElements() {
|
||||
return this.elements;
|
||||
}
|
||||
|
||||
public setNewElementType(type: string) {
|
||||
if (this.latestElement !== undefined) {
|
||||
this.latestElement.type = type;
|
||||
}
|
||||
}
|
||||
|
||||
public setNewElementDocRef(docRef: string) {
|
||||
if (this.latestElement !== undefined) {
|
||||
this.latestElement.docRef = docRef;
|
||||
}
|
||||
}
|
||||
|
||||
public addRelationship(type: RelationshipType, src: string, dst: string) {
|
||||
this.relations.push({
|
||||
type,
|
||||
src,
|
||||
dst,
|
||||
});
|
||||
}
|
||||
|
||||
public getRelationships() {
|
||||
return this.relations;
|
||||
}
|
||||
|
||||
public clear() {
|
||||
this.relations = [];
|
||||
this.resetLatestRequirement();
|
||||
this.requirements = new Map();
|
||||
this.resetLatestElement();
|
||||
this.elements = new Map();
|
||||
this.classes = new Map();
|
||||
commonClear();
|
||||
}
|
||||
|
||||
public setCssStyle(ids: string[], styles: string[]) {
|
||||
for (const id of ids) {
|
||||
const node = this.requirements.get(id) ?? this.elements.get(id);
|
||||
if (!styles || !node) {
|
||||
return;
|
||||
}
|
||||
for (const s of styles) {
|
||||
if (s.includes(',')) {
|
||||
node.cssStyles.push(...s.split(','));
|
||||
} else {
|
||||
node.cssStyles.push(s);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public setClass(ids: string[], classNames: string[]) {
|
||||
for (const id of ids) {
|
||||
const node = this.requirements.get(id) ?? this.elements.get(id);
|
||||
if (node) {
|
||||
for (const _class of classNames) {
|
||||
node.classes.push(_class);
|
||||
const styles = this.classes.get(_class)?.styles;
|
||||
if (styles) {
|
||||
node.cssStyles.push(...styles);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public defineClass(ids: string[], style: string[]) {
|
||||
for (const id of ids) {
|
||||
let styleClass = this.classes.get(id);
|
||||
if (styleClass === undefined) {
|
||||
styleClass = { id, styles: [], textStyles: [] };
|
||||
this.classes.set(id, styleClass);
|
||||
}
|
||||
|
||||
if (style) {
|
||||
style.forEach(function (s) {
|
||||
if (/color/.exec(s)) {
|
||||
const newStyle = s.replace('fill', 'bgFill'); // .replace('color', 'fill');
|
||||
styleClass.textStyles.push(newStyle);
|
||||
}
|
||||
styleClass.styles.push(s);
|
||||
});
|
||||
}
|
||||
|
||||
this.requirements.forEach((value) => {
|
||||
if (value.classes.includes(id)) {
|
||||
value.cssStyles.push(...style.flatMap((s) => s.split(',')));
|
||||
}
|
||||
});
|
||||
this.elements.forEach((value) => {
|
||||
if (value.classes.includes(id)) {
|
||||
value.cssStyles.push(...style.flatMap((s) => s.split(',')));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public getClasses() {
|
||||
return this.classes;
|
||||
}
|
||||
|
||||
public getData() {
|
||||
const config = getConfig();
|
||||
const nodes: Node[] = [];
|
||||
const edges: Edge[] = [];
|
||||
for (const requirement of this.requirements.values()) {
|
||||
const node = requirement as unknown as Node;
|
||||
node.id = requirement.name;
|
||||
node.cssStyles = requirement.cssStyles;
|
||||
node.cssClasses = requirement.classes.join(' ');
|
||||
node.shape = 'requirementBox';
|
||||
node.look = config.look;
|
||||
nodes.push(node);
|
||||
}
|
||||
|
||||
for (const element of this.elements.values()) {
|
||||
const node = element as unknown as Node;
|
||||
node.shape = 'requirementBox';
|
||||
node.look = config.look;
|
||||
node.id = element.name;
|
||||
node.cssStyles = element.cssStyles;
|
||||
node.cssClasses = element.classes.join(' ');
|
||||
|
||||
nodes.push(node);
|
||||
}
|
||||
|
||||
for (const relation of this.relations) {
|
||||
let counter = 0;
|
||||
const isContains = relation.type === this.Relationships.CONTAINS;
|
||||
const edge: Edge = {
|
||||
id: `${relation.src}-${relation.dst}-${counter}`,
|
||||
start: this.requirements.get(relation.src)?.name ?? this.elements.get(relation.src)?.name,
|
||||
end: this.requirements.get(relation.dst)?.name ?? this.elements.get(relation.dst)?.name,
|
||||
label: `<<${relation.type}>>`,
|
||||
classes: 'relationshipLine',
|
||||
style: ['fill:none', isContains ? '' : 'stroke-dasharray: 10,7'],
|
||||
labelpos: 'c',
|
||||
thickness: 'normal',
|
||||
type: 'normal',
|
||||
pattern: isContains ? 'normal' : 'dashed',
|
||||
arrowTypeEnd: isContains ? 'requirement_contains' : 'requirement_arrow',
|
||||
look: config.look,
|
||||
};
|
||||
|
||||
edges.push(edge);
|
||||
counter++;
|
||||
}
|
||||
|
||||
return { nodes, edges, other: {}, config, direction: this.getDirection() };
|
||||
}
|
||||
|
||||
public setAccTitle = setAccTitle;
|
||||
public getAccTitle = getAccTitle;
|
||||
public setAccDescription = setAccDescription;
|
||||
public getAccDescription = getAccDescription;
|
||||
public setDiagramTitle = setDiagramTitle;
|
||||
public getDiagramTitle = getDiagramTitle;
|
||||
public getConfig = () => getConfig().requirement;
|
||||
}
|
||||
@@ -1,13 +1,15 @@
|
||||
import type { DiagramDefinition } from '../../diagram-api/types.js';
|
||||
// @ts-ignore: JISON doesn't support types
|
||||
import parser from './parser/requirementDiagram.jison';
|
||||
import db from './requirementDb.js';
|
||||
import { RequirementDB } from './requirementDb.js';
|
||||
import styles from './styles.js';
|
||||
import renderer from './requirementRenderer.js';
|
||||
import * as renderer from './requirementRenderer.js';
|
||||
|
||||
export const diagram: DiagramDefinition = {
|
||||
parser,
|
||||
db,
|
||||
get db() {
|
||||
return new RequirementDB();
|
||||
},
|
||||
renderer,
|
||||
styles,
|
||||
};
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
const ReqMarkers = {
|
||||
CONTAINS: 'contains',
|
||||
ARROW: 'arrow',
|
||||
};
|
||||
|
||||
const insertLineEndings = (parentNode, conf) => {
|
||||
let containsNode = parentNode
|
||||
.append('defs')
|
||||
.append('marker')
|
||||
.attr('id', ReqMarkers.CONTAINS + '_line_ending')
|
||||
.attr('refX', 0)
|
||||
.attr('refY', conf.line_height / 2)
|
||||
.attr('markerWidth', conf.line_height)
|
||||
.attr('markerHeight', conf.line_height)
|
||||
.attr('orient', 'auto')
|
||||
.append('g');
|
||||
|
||||
containsNode
|
||||
.append('circle')
|
||||
.attr('cx', conf.line_height / 2)
|
||||
.attr('cy', conf.line_height / 2)
|
||||
.attr('r', conf.line_height / 2)
|
||||
// .attr('stroke', conf.rect_border_color)
|
||||
// .attr('stroke-width', 1)
|
||||
.attr('fill', 'none');
|
||||
|
||||
containsNode
|
||||
.append('line')
|
||||
.attr('x1', 0)
|
||||
.attr('x2', conf.line_height)
|
||||
.attr('y1', conf.line_height / 2)
|
||||
.attr('y2', conf.line_height / 2)
|
||||
// .attr('stroke', conf.rect_border_color)
|
||||
.attr('stroke-width', 1);
|
||||
|
||||
containsNode
|
||||
.append('line')
|
||||
.attr('y1', 0)
|
||||
.attr('y2', conf.line_height)
|
||||
.attr('x1', conf.line_height / 2)
|
||||
.attr('x2', conf.line_height / 2)
|
||||
// .attr('stroke', conf.rect_border_color)
|
||||
.attr('stroke-width', 1);
|
||||
|
||||
parentNode
|
||||
.append('defs')
|
||||
.append('marker')
|
||||
.attr('id', ReqMarkers.ARROW + '_line_ending')
|
||||
.attr('refX', conf.line_height)
|
||||
.attr('refY', 0.5 * conf.line_height)
|
||||
.attr('markerWidth', conf.line_height)
|
||||
.attr('markerHeight', conf.line_height)
|
||||
.attr('orient', 'auto')
|
||||
.append('path')
|
||||
.attr(
|
||||
'd',
|
||||
`M0,0
|
||||
L${conf.line_height},${conf.line_height / 2}
|
||||
M${conf.line_height},${conf.line_height / 2}
|
||||
L0,${conf.line_height}`
|
||||
)
|
||||
.attr('stroke-width', 1);
|
||||
// .attr('stroke', conf.rect_border_color);
|
||||
};
|
||||
|
||||
export default {
|
||||
ReqMarkers,
|
||||
insertLineEndings,
|
||||
};
|
||||
@@ -1,377 +0,0 @@
|
||||
import { line, select } from 'd3';
|
||||
import { layout as dagreLayout } from 'dagre-d3-es/src/dagre/index.js';
|
||||
import * as graphlib from 'dagre-d3-es/src/graphlib/index.js';
|
||||
import { getConfig } from '../../diagram-api/diagramAPI.js';
|
||||
import { log } from '../../logger.js';
|
||||
import { configureSvgSize } from '../../setupGraphViewbox.js';
|
||||
import common from '../common/common.js';
|
||||
import markers from './requirementMarkers.js';
|
||||
|
||||
let conf = {};
|
||||
let relCnt = 0;
|
||||
|
||||
const newRectNode = (parentNode, id) => {
|
||||
return parentNode
|
||||
.insert('rect', '#' + id)
|
||||
.attr('class', 'req reqBox')
|
||||
.attr('x', 0)
|
||||
.attr('y', 0)
|
||||
.attr('width', conf.rect_min_width + 'px')
|
||||
.attr('height', conf.rect_min_height + 'px');
|
||||
};
|
||||
|
||||
const newTitleNode = (parentNode, id, txts) => {
|
||||
let x = conf.rect_min_width / 2;
|
||||
|
||||
let title = parentNode
|
||||
.append('text')
|
||||
.attr('class', 'req reqLabel reqTitle')
|
||||
.attr('id', id)
|
||||
.attr('x', x)
|
||||
.attr('y', conf.rect_padding)
|
||||
.attr('dominant-baseline', 'hanging');
|
||||
// .attr(
|
||||
// 'style',
|
||||
// 'font-family: ' + configApi.getConfig().fontFamily + '; font-size: ' + conf.fontSize + 'px'
|
||||
// )
|
||||
let i = 0;
|
||||
txts.forEach((textStr) => {
|
||||
if (i == 0) {
|
||||
title
|
||||
.append('tspan')
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('x', conf.rect_min_width / 2)
|
||||
.attr('dy', 0)
|
||||
.text(textStr);
|
||||
} else {
|
||||
title
|
||||
.append('tspan')
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('x', conf.rect_min_width / 2)
|
||||
.attr('dy', conf.line_height * 0.75)
|
||||
.text(textStr);
|
||||
}
|
||||
i++;
|
||||
});
|
||||
|
||||
let yPadding = 1.5 * conf.rect_padding;
|
||||
let linePadding = i * conf.line_height * 0.75;
|
||||
let totalY = yPadding + linePadding;
|
||||
|
||||
parentNode
|
||||
.append('line')
|
||||
.attr('class', 'req-title-line')
|
||||
.attr('x1', '0')
|
||||
.attr('x2', conf.rect_min_width)
|
||||
.attr('y1', totalY)
|
||||
.attr('y2', totalY);
|
||||
|
||||
return {
|
||||
titleNode: title,
|
||||
y: totalY,
|
||||
};
|
||||
};
|
||||
|
||||
const newBodyNode = (parentNode, id, txts, yStart) => {
|
||||
let body = parentNode
|
||||
.append('text')
|
||||
.attr('class', 'req reqLabel')
|
||||
.attr('id', id)
|
||||
.attr('x', conf.rect_padding)
|
||||
.attr('y', yStart)
|
||||
.attr('dominant-baseline', 'hanging');
|
||||
// .attr(
|
||||
// 'style',
|
||||
// 'font-family: ' + configApi.getConfig().fontFamily + '; font-size: ' + conf.fontSize + 'px'
|
||||
// );
|
||||
|
||||
let currentRow = 0;
|
||||
const charLimit = 30;
|
||||
let wrappedTxts = [];
|
||||
txts.forEach((textStr) => {
|
||||
let currentTextLen = textStr.length;
|
||||
while (currentTextLen > charLimit && currentRow < 3) {
|
||||
let firstPart = textStr.substring(0, charLimit);
|
||||
textStr = textStr.substring(charLimit, textStr.length);
|
||||
currentTextLen = textStr.length;
|
||||
wrappedTxts[wrappedTxts.length] = firstPart;
|
||||
currentRow++;
|
||||
}
|
||||
if (currentRow == 3) {
|
||||
let lastStr = wrappedTxts[wrappedTxts.length - 1];
|
||||
wrappedTxts[wrappedTxts.length - 1] = lastStr.substring(0, lastStr.length - 4) + '...';
|
||||
} else {
|
||||
wrappedTxts[wrappedTxts.length] = textStr;
|
||||
}
|
||||
currentRow = 0;
|
||||
});
|
||||
|
||||
wrappedTxts.forEach((textStr) => {
|
||||
body.append('tspan').attr('x', conf.rect_padding).attr('dy', conf.line_height).text(textStr);
|
||||
});
|
||||
|
||||
return body;
|
||||
};
|
||||
|
||||
const addEdgeLabel = (parentNode, svgPath, conf, txt) => {
|
||||
// Find the half-way point
|
||||
const len = svgPath.node().getTotalLength();
|
||||
const labelPoint = svgPath.node().getPointAtLength(len * 0.5);
|
||||
|
||||
// Append a text node containing the label
|
||||
const labelId = 'rel' + relCnt;
|
||||
relCnt++;
|
||||
|
||||
const labelNode = parentNode
|
||||
.append('text')
|
||||
.attr('class', 'req relationshipLabel')
|
||||
.attr('id', labelId)
|
||||
.attr('x', labelPoint.x)
|
||||
.attr('y', labelPoint.y)
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('dominant-baseline', 'middle')
|
||||
// .attr('style', 'font-family: ' + conf.fontFamily + '; font-size: ' + conf.fontSize + 'px')
|
||||
.text(txt);
|
||||
|
||||
// Figure out how big the opaque 'container' rectangle needs to be
|
||||
const labelBBox = labelNode.node().getBBox();
|
||||
|
||||
// Insert the opaque rectangle before the text label
|
||||
parentNode
|
||||
.insert('rect', '#' + labelId)
|
||||
.attr('class', 'req reqLabelBox')
|
||||
.attr('x', labelPoint.x - labelBBox.width / 2)
|
||||
.attr('y', labelPoint.y - labelBBox.height / 2)
|
||||
.attr('width', labelBBox.width)
|
||||
.attr('height', labelBBox.height)
|
||||
.attr('fill', 'white')
|
||||
.attr('fill-opacity', '85%');
|
||||
};
|
||||
|
||||
const drawRelationshipFromLayout = function (svg, rel, g, insert, diagObj) {
|
||||
// Find the edge relating to this relationship
|
||||
const edge = g.edge(elementString(rel.src), elementString(rel.dst));
|
||||
|
||||
// Get a function that will generate the line path
|
||||
const lineFunction = line()
|
||||
.x(function (d) {
|
||||
return d.x;
|
||||
})
|
||||
.y(function (d) {
|
||||
return d.y;
|
||||
});
|
||||
|
||||
// Insert the line at the right place
|
||||
const svgPath = svg
|
||||
.insert('path', '#' + insert)
|
||||
.attr('class', 'er relationshipLine')
|
||||
.attr('d', lineFunction(edge.points))
|
||||
.attr('fill', 'none');
|
||||
|
||||
if (rel.type == diagObj.db.Relationships.CONTAINS) {
|
||||
svgPath.attr(
|
||||
'marker-start',
|
||||
'url(' + common.getUrl(conf.arrowMarkerAbsolute) + '#' + rel.type + '_line_ending' + ')'
|
||||
);
|
||||
} else {
|
||||
svgPath.attr('stroke-dasharray', '10,7');
|
||||
svgPath.attr(
|
||||
'marker-end',
|
||||
'url(' +
|
||||
common.getUrl(conf.arrowMarkerAbsolute) +
|
||||
'#' +
|
||||
markers.ReqMarkers.ARROW +
|
||||
'_line_ending' +
|
||||
')'
|
||||
);
|
||||
}
|
||||
|
||||
addEdgeLabel(svg, svgPath, conf, `<<${rel.type}>>`);
|
||||
|
||||
return;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Map<string, any>} reqs
|
||||
* @param graph
|
||||
* @param svgNode
|
||||
*/
|
||||
export const drawReqs = (reqs, graph, svgNode) => {
|
||||
reqs.forEach((req, reqName) => {
|
||||
reqName = elementString(reqName);
|
||||
log.info('Added new requirement: ', reqName);
|
||||
|
||||
const groupNode = svgNode.append('g').attr('id', reqName);
|
||||
const textId = 'req-' + reqName;
|
||||
const rectNode = newRectNode(groupNode, textId);
|
||||
|
||||
let nodes = [];
|
||||
|
||||
let titleNodeInfo = newTitleNode(groupNode, reqName + '_title', [
|
||||
`<<${req.type}>>`,
|
||||
`${req.name}`,
|
||||
]);
|
||||
|
||||
nodes.push(titleNodeInfo.titleNode);
|
||||
|
||||
let bodyNode = newBodyNode(
|
||||
groupNode,
|
||||
reqName + '_body',
|
||||
[
|
||||
`Id: ${req.id}`,
|
||||
`Text: ${req.text}`,
|
||||
`Risk: ${req.risk}`,
|
||||
`Verification: ${req.verifyMethod}`,
|
||||
],
|
||||
titleNodeInfo.y
|
||||
);
|
||||
|
||||
nodes.push(bodyNode);
|
||||
|
||||
const rectBBox = rectNode.node().getBBox();
|
||||
|
||||
// Add the entity to the graph
|
||||
graph.setNode(reqName, {
|
||||
width: rectBBox.width,
|
||||
height: rectBBox.height,
|
||||
shape: 'rect',
|
||||
id: reqName,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Map<string, any>} els
|
||||
* @param graph
|
||||
* @param svgNode
|
||||
*/
|
||||
export const drawElements = (els, graph, svgNode) => {
|
||||
els.forEach((el, elName) => {
|
||||
const id = elementString(elName);
|
||||
|
||||
const groupNode = svgNode.append('g').attr('id', id);
|
||||
const textId = 'element-' + id;
|
||||
const rectNode = newRectNode(groupNode, textId);
|
||||
|
||||
let nodes = [];
|
||||
|
||||
let titleNodeInfo = newTitleNode(groupNode, textId + '_title', [`<<Element>>`, `${elName}`]);
|
||||
|
||||
nodes.push(titleNodeInfo.titleNode);
|
||||
|
||||
let bodyNode = newBodyNode(
|
||||
groupNode,
|
||||
textId + '_body',
|
||||
[`Type: ${el.type || 'Not Specified'}`, `Doc Ref: ${el.docRef || 'None'}`],
|
||||
titleNodeInfo.y
|
||||
);
|
||||
|
||||
nodes.push(bodyNode);
|
||||
|
||||
const rectBBox = rectNode.node().getBBox();
|
||||
|
||||
// Add the entity to the graph
|
||||
graph.setNode(id, {
|
||||
width: rectBBox.width,
|
||||
height: rectBBox.height,
|
||||
shape: 'rect',
|
||||
id: id,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const addRelationships = (relationships, g) => {
|
||||
relationships.forEach(function (r) {
|
||||
let src = elementString(r.src);
|
||||
let dst = elementString(r.dst);
|
||||
g.setEdge(src, dst, { relationship: r });
|
||||
});
|
||||
return relationships;
|
||||
};
|
||||
|
||||
const adjustEntities = function (svgNode, graph) {
|
||||
graph.nodes().forEach(function (v) {
|
||||
if (v !== undefined && graph.node(v) !== undefined) {
|
||||
svgNode.select('#' + v);
|
||||
svgNode
|
||||
.select('#' + v)
|
||||
.attr(
|
||||
'transform',
|
||||
'translate(' +
|
||||
(graph.node(v).x - graph.node(v).width / 2) +
|
||||
',' +
|
||||
(graph.node(v).y - graph.node(v).height / 2) +
|
||||
' )'
|
||||
);
|
||||
}
|
||||
});
|
||||
return;
|
||||
};
|
||||
|
||||
const elementString = (str) => {
|
||||
return str.replace(/\s/g, '').replace(/\./g, '_');
|
||||
};
|
||||
|
||||
export const draw = (text, id, _version, diagObj) => {
|
||||
conf = getConfig().requirement;
|
||||
|
||||
const securityLevel = conf.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');
|
||||
|
||||
const svg = root.select(`[id='${id}']`);
|
||||
markers.insertLineEndings(svg, conf);
|
||||
|
||||
const g = new graphlib.Graph({
|
||||
multigraph: false,
|
||||
compound: false,
|
||||
directed: true,
|
||||
})
|
||||
.setGraph({
|
||||
rankdir: conf.layoutDirection,
|
||||
marginx: 20,
|
||||
marginy: 20,
|
||||
nodesep: 100,
|
||||
edgesep: 100,
|
||||
ranksep: 100,
|
||||
})
|
||||
.setDefaultEdgeLabel(function () {
|
||||
return {};
|
||||
});
|
||||
|
||||
let requirements = diagObj.db.getRequirements();
|
||||
let elements = diagObj.db.getElements();
|
||||
let relationships = diagObj.db.getRelationships();
|
||||
|
||||
drawReqs(requirements, g, svg);
|
||||
drawElements(elements, g, svg);
|
||||
addRelationships(relationships, g);
|
||||
dagreLayout(g);
|
||||
adjustEntities(svg, g);
|
||||
|
||||
relationships.forEach(function (rel) {
|
||||
drawRelationshipFromLayout(svg, rel, g, id, diagObj);
|
||||
});
|
||||
|
||||
const padding = conf.rect_padding;
|
||||
const svgBounds = svg.node().getBBox();
|
||||
const width = svgBounds.width + padding * 2;
|
||||
const height = svgBounds.height + padding * 2;
|
||||
|
||||
configureSvgSize(svg, height, width, conf.useMaxWidth);
|
||||
|
||||
svg.attr('viewBox', `${svgBounds.x - padding} ${svgBounds.y - padding} ${width} ${height}`);
|
||||
};
|
||||
|
||||
// cspell:ignore txts
|
||||
|
||||
export default {
|
||||
draw,
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
import { getConfig } from '../../diagram-api/diagramAPI.js';
|
||||
import { log } from '../../logger.js';
|
||||
import { getDiagramElement } from '../../rendering-util/insertElementsForSize.js';
|
||||
import { getRegisteredLayoutAlgorithm, render } from '../../rendering-util/render.js';
|
||||
import { setupViewPortForSVG } from '../../rendering-util/setupViewPortForSVG.js';
|
||||
import type { LayoutData } from '../../rendering-util/types.js';
|
||||
import utils from '../../utils.js';
|
||||
|
||||
export const draw = async function (text: string, id: string, _version: string, diag: any) {
|
||||
log.info('REF0:');
|
||||
log.info('Drawing requirement diagram (unified)', id);
|
||||
const { securityLevel, state: conf, layout } = getConfig();
|
||||
|
||||
const data4Layout = diag.db.getData() as LayoutData;
|
||||
|
||||
// Create the root SVG - the element is the div containing the SVG element
|
||||
const svg = getDiagramElement(id, securityLevel);
|
||||
|
||||
data4Layout.type = diag.type;
|
||||
data4Layout.layoutAlgorithm = getRegisteredLayoutAlgorithm(layout);
|
||||
|
||||
data4Layout.nodeSpacing = conf?.nodeSpacing ?? 50;
|
||||
data4Layout.rankSpacing = conf?.rankSpacing ?? 50;
|
||||
data4Layout.markers = ['requirement_contains', 'requirement_arrow'];
|
||||
data4Layout.diagramId = id;
|
||||
await render(data4Layout, svg);
|
||||
const padding = 8;
|
||||
utils.insertTitle(
|
||||
svg,
|
||||
'requirementDiagramTitleText',
|
||||
conf?.titleTopMargin ?? 25,
|
||||
diag.db.getDiagramTitle()
|
||||
);
|
||||
|
||||
setupViewPortForSVG(svg, padding, 'requirementDiagram', conf?.useMaxWidth ?? true);
|
||||
};
|
||||
@@ -40,6 +40,21 @@ const getStyles = (options) => `
|
||||
.relationshipLabel {
|
||||
fill: ${options.relationLabelColor};
|
||||
}
|
||||
.divider {
|
||||
stroke: ${options.nodeBorder};
|
||||
stroke-width: 1;
|
||||
}
|
||||
.label {
|
||||
font-family: ${options.fontFamily};
|
||||
color: ${options.nodeTextColor || options.textColor};
|
||||
}
|
||||
.label text,span {
|
||||
fill: ${options.nodeTextColor || options.textColor};
|
||||
color: ${options.nodeTextColor || options.textColor};
|
||||
}
|
||||
.labelBkg {
|
||||
background-color: ${options.edgeLabelBackground};
|
||||
}
|
||||
|
||||
`;
|
||||
// fill', conf.rect_fill)
|
||||
|
||||
51
packages/mermaid/src/diagrams/requirement/types.ts
Normal file
51
packages/mermaid/src/diagrams/requirement/types.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
export type RequirementType =
|
||||
| 'Requirement'
|
||||
| 'Functional Requirement'
|
||||
| 'Interface Requirement'
|
||||
| 'Performance Requirement'
|
||||
| 'Physical Requirement'
|
||||
| 'Design Constraint';
|
||||
|
||||
export type RiskLevel = 'Low' | 'Medium' | 'High';
|
||||
|
||||
export type VerifyType = 'Analysis' | 'Demonstration' | 'Inspection' | 'Test';
|
||||
|
||||
export interface Requirement {
|
||||
name: string;
|
||||
type: RequirementType;
|
||||
requirementId: string;
|
||||
text: string;
|
||||
risk: RiskLevel;
|
||||
verifyMethod: VerifyType;
|
||||
cssStyles: string[];
|
||||
classes: string[];
|
||||
}
|
||||
|
||||
export type RelationshipType =
|
||||
| 'contains'
|
||||
| 'copies'
|
||||
| 'derives'
|
||||
| 'satisfies'
|
||||
| 'verifies'
|
||||
| 'refines'
|
||||
| 'traces';
|
||||
|
||||
export interface Relation {
|
||||
type: RelationshipType;
|
||||
src: string;
|
||||
dst: string;
|
||||
}
|
||||
|
||||
export interface Element {
|
||||
name: string;
|
||||
type: string;
|
||||
docRef: string;
|
||||
cssStyles: string[];
|
||||
classes: string[];
|
||||
}
|
||||
|
||||
export interface RequirementClass {
|
||||
id: string;
|
||||
styles: string[];
|
||||
textStyles: string[];
|
||||
}
|
||||
@@ -4,11 +4,13 @@ import parser from './parser/sankey.jison';
|
||||
import db from './sankeyDB.js';
|
||||
import renderer from './sankeyRenderer.js';
|
||||
import { prepareTextForParsing } from './sankeyUtils.js';
|
||||
import sankeyStyles from './styles.js';
|
||||
|
||||
const originalParse = parser.parse.bind(parser);
|
||||
parser.parse = (text: string) => originalParse(prepareTextForParsing(text));
|
||||
|
||||
export const diagram: DiagramDefinition = {
|
||||
styles: sankeyStyles,
|
||||
parser,
|
||||
db,
|
||||
renderer,
|
||||
|
||||
@@ -136,7 +136,6 @@ export const draw = function (text: string, id: string, _version: string, diagOb
|
||||
svg
|
||||
.append('g')
|
||||
.attr('class', 'node-labels')
|
||||
.attr('font-family', 'sans-serif')
|
||||
.attr('font-size', 14)
|
||||
.selectAll('text')
|
||||
.data(graph.nodes)
|
||||
|
||||
6
packages/mermaid/src/diagrams/sankey/styles.js
Normal file
6
packages/mermaid/src/diagrams/sankey/styles.js
Normal file
@@ -0,0 +1,6 @@
|
||||
const getStyles = (options) =>
|
||||
`.label {
|
||||
font-family: ${options.fontFamily};
|
||||
}`;
|
||||
|
||||
export default getStyles;
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,16 +1,26 @@
|
||||
import type { DiagramDefinition } from '../../diagram-api/types.js';
|
||||
// @ts-ignore: JISON doesn't support types
|
||||
import parser from './parser/sequenceDiagram.jison';
|
||||
import db from './sequenceDb.js';
|
||||
import { SequenceDB } from './sequenceDb.js';
|
||||
import styles from './styles.js';
|
||||
import { setConfig } from '../../diagram-api/diagramAPI.js';
|
||||
import renderer from './sequenceRenderer.js';
|
||||
import type { MermaidConfig } from '../../config.type.js';
|
||||
|
||||
export const diagram: DiagramDefinition = {
|
||||
parser,
|
||||
db,
|
||||
get db() {
|
||||
return new SequenceDB();
|
||||
},
|
||||
renderer,
|
||||
styles,
|
||||
init: ({ wrap }) => {
|
||||
db.setWrap(wrap);
|
||||
init: (cnf: MermaidConfig) => {
|
||||
if (!cnf.sequence) {
|
||||
cnf.sequence = {};
|
||||
}
|
||||
if (cnf.wrap) {
|
||||
cnf.sequence.wrap = cnf.wrap;
|
||||
setConfig({ sequence: { wrap: cnf.wrap } });
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1538,7 +1538,6 @@ const calculateLoopBounds = async function (messages, actors, _maxWidthPerActor,
|
||||
let current, noteModel, msgModel;
|
||||
|
||||
for (const msg of messages) {
|
||||
msg.id = utils.random({ length: 10 });
|
||||
switch (msg.type) {
|
||||
case diagObj.db.LINETYPE.LOOP_START:
|
||||
case diagObj.db.LINETYPE.ALT_START:
|
||||
|
||||
@@ -20,6 +20,7 @@ export interface Actor {
|
||||
}
|
||||
|
||||
export interface Message {
|
||||
id: string;
|
||||
from?: string;
|
||||
to?: string;
|
||||
message:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import stateDb from '../stateDb.js';
|
||||
import { StateDB } from '../stateDb.js';
|
||||
import stateDiagram from './stateDiagram.jison';
|
||||
import { setConfig } from '../../../config.js';
|
||||
|
||||
@@ -7,7 +7,9 @@ setConfig({
|
||||
});
|
||||
|
||||
describe('state parser can parse...', () => {
|
||||
let stateDb;
|
||||
beforeEach(function () {
|
||||
stateDb = new StateDB(2);
|
||||
stateDiagram.parser.yy = stateDb;
|
||||
stateDiagram.parser.yy.clear();
|
||||
});
|
||||
@@ -18,7 +20,6 @@ describe('state parser can parse...', () => {
|
||||
const diagramText = `stateDiagram-v2
|
||||
state "Small State 1" as namedState1`;
|
||||
stateDiagram.parser.parse(diagramText);
|
||||
stateDiagram.parser.yy.extract(stateDiagram.parser.yy.getRootDocV2());
|
||||
|
||||
const states = stateDiagram.parser.yy.getStates();
|
||||
expect(states.get('namedState1')).not.toBeUndefined();
|
||||
@@ -31,7 +32,6 @@ describe('state parser can parse...', () => {
|
||||
const diagramText = `stateDiagram-v2
|
||||
namedState1 : Small State 1`;
|
||||
stateDiagram.parser.parse(diagramText);
|
||||
stateDiagram.parser.yy.extract(stateDiagram.parser.yy.getRootDocV2());
|
||||
|
||||
const states = stateDiagram.parser.yy.getStates();
|
||||
expect(states.get('namedState1')).not.toBeUndefined();
|
||||
@@ -42,7 +42,6 @@ describe('state parser can parse...', () => {
|
||||
const diagramText = `stateDiagram-v2
|
||||
namedState1:Small State 1`;
|
||||
stateDiagram.parser.parse(diagramText);
|
||||
stateDiagram.parser.yy.extract(stateDiagram.parser.yy.getRootDocV2());
|
||||
|
||||
const states = stateDiagram.parser.yy.getStates();
|
||||
expect(states.get('namedState1')).not.toBeUndefined();
|
||||
@@ -60,7 +59,6 @@ describe('state parser can parse...', () => {
|
||||
state assemblies
|
||||
`;
|
||||
stateDiagram.parser.parse(diagramText);
|
||||
stateDiagram.parser.yy.extract(stateDiagram.parser.yy.getRootDocV2());
|
||||
const states = stateDiagram.parser.yy.getStates();
|
||||
expect(states.get('assemble')).not.toBeUndefined();
|
||||
expect(states.get('assemblies')).not.toBeUndefined();
|
||||
@@ -71,7 +69,6 @@ describe('state parser can parse...', () => {
|
||||
state "as" as as
|
||||
`;
|
||||
stateDiagram.parser.parse(diagramText);
|
||||
stateDiagram.parser.yy.extract(stateDiagram.parser.yy.getRootDocV2());
|
||||
const states = stateDiagram.parser.yy.getStates();
|
||||
expect(states.get('as')).not.toBeUndefined();
|
||||
expect(states.get('as').descriptions.join(' ')).toEqual('as');
|
||||
@@ -96,7 +93,6 @@ describe('state parser can parse...', () => {
|
||||
namedState2 --> bigState2: should point to \\nbigState2 container`;
|
||||
|
||||
stateDiagram.parser.parse(diagramText);
|
||||
stateDiagram.parser.yy.extract(stateDiagram.parser.yy.getRootDocV2());
|
||||
|
||||
const states = stateDiagram.parser.yy.getStates();
|
||||
expect(states.get('namedState1')).not.toBeUndefined();
|
||||
@@ -120,7 +116,6 @@ describe('state parser can parse...', () => {
|
||||
inner1 --> inner2
|
||||
}`;
|
||||
stateDiagram.parser.parse(diagramText);
|
||||
stateDiagram.parser.yy.extract(stateDiagram.parser.yy.getRootDocV2());
|
||||
|
||||
const states = stateDiagram.parser.yy.getStates();
|
||||
expect(states.get('bigState1')).not.toBeUndefined();
|
||||
@@ -137,7 +132,6 @@ describe('state parser can parse...', () => {
|
||||
stateDiagram-v2
|
||||
[*] --> ${prop}
|
||||
${prop} --> [*]`);
|
||||
stateDiagram.parser.yy.extract(stateDiagram.parser.yy.getRootDocV2());
|
||||
const states = stateDiagram.parser.yy.getStates();
|
||||
expect(states.get(prop)).not.toBeUndefined();
|
||||
});
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import stateDb from '../stateDb.js';
|
||||
import stateDiagram from './stateDiagram.jison';
|
||||
import { setConfig } from '../../../config.js';
|
||||
import { StateDB } from '../stateDb.js';
|
||||
import stateDiagram from './stateDiagram.jison';
|
||||
|
||||
setConfig({
|
||||
securityLevel: 'strict',
|
||||
});
|
||||
|
||||
describe('ClassDefs and classes when parsing a State diagram', () => {
|
||||
let stateDb;
|
||||
beforeEach(function () {
|
||||
stateDb = new StateDB(2);
|
||||
stateDiagram.parser.yy = stateDb;
|
||||
stateDiagram.parser.yy.clear();
|
||||
});
|
||||
@@ -16,7 +18,6 @@ describe('ClassDefs and classes when parsing a State diagram', () => {
|
||||
describe('defining (classDef)', () => {
|
||||
it('has "classDef" as a keyword, an id, and can set a css style attribute', function () {
|
||||
stateDiagram.parser.parse('stateDiagram-v2\n classDef exampleClass background:#bbb;');
|
||||
stateDiagram.parser.yy.extract(stateDiagram.parser.yy.getRootDocV2());
|
||||
|
||||
const styleClasses = stateDb.getClasses();
|
||||
expect(styleClasses.get('exampleClass').styles.length).toEqual(1);
|
||||
@@ -27,7 +28,6 @@ describe('ClassDefs and classes when parsing a State diagram', () => {
|
||||
stateDiagram.parser.parse(
|
||||
'stateDiagram-v2\n classDef exampleClass background:#bbb, font-weight:bold, font-style:italic;'
|
||||
);
|
||||
stateDiagram.parser.yy.extract(stateDiagram.parser.yy.getRootDocV2());
|
||||
|
||||
const styleClasses = stateDb.getClasses();
|
||||
expect(styleClasses.get('exampleClass').styles.length).toEqual(3);
|
||||
@@ -41,7 +41,6 @@ describe('ClassDefs and classes when parsing a State diagram', () => {
|
||||
stateDiagram.parser.parse(
|
||||
'stateDiagram-v2\n classDef exampleStyleClass background:#bbb,border:1.5px solid red;'
|
||||
);
|
||||
stateDiagram.parser.yy.extract(stateDiagram.parser.yy.getRootDocV2());
|
||||
|
||||
const classes = stateDiagram.parser.yy.getClasses();
|
||||
expect(classes.get('exampleStyleClass').styles.length).toBe(2);
|
||||
@@ -53,7 +52,6 @@ describe('ClassDefs and classes when parsing a State diagram', () => {
|
||||
stateDiagram.parser.parse(
|
||||
'stateDiagram-v2\n classDef exampleStyleClass background: #bbb,border:1.5px solid red;'
|
||||
);
|
||||
stateDiagram.parser.yy.extract(stateDiagram.parser.yy.getRootDocV2());
|
||||
|
||||
const classes = stateDiagram.parser.yy.getClasses();
|
||||
expect(classes.get('exampleStyleClass').styles.length).toBe(2);
|
||||
@@ -65,7 +63,6 @@ describe('ClassDefs and classes when parsing a State diagram', () => {
|
||||
stateDiagram.parser.parse(
|
||||
'stateDiagram-v2\n classDef __proto__ background:#bbb,border:1.5px solid red;\n classDef constructor background:#bbb,border:1.5px solid red;'
|
||||
);
|
||||
stateDiagram.parser.yy.extract(stateDiagram.parser.yy.getRootDocV2());
|
||||
const classes = stateDiagram.parser.yy.getClasses();
|
||||
expect(classes.get('__proto__').styles.length).toBe(2);
|
||||
expect(classes.get('constructor').styles.length).toBe(2);
|
||||
@@ -81,7 +78,6 @@ describe('ClassDefs and classes when parsing a State diagram', () => {
|
||||
diagram += 'class a exampleStyleClass';
|
||||
|
||||
stateDiagram.parser.parse(diagram);
|
||||
stateDiagram.parser.yy.extract(stateDiagram.parser.yy.getRootDocV2());
|
||||
|
||||
const classes = stateDb.getClasses();
|
||||
expect(classes.get('exampleStyleClass').styles.length).toEqual(2);
|
||||
@@ -102,7 +98,6 @@ describe('ClassDefs and classes when parsing a State diagram', () => {
|
||||
diagram += 'class a_a exampleStyleClass';
|
||||
|
||||
stateDiagram.parser.parse(diagram);
|
||||
stateDiagram.parser.yy.extract(stateDiagram.parser.yy.getRootDocV2());
|
||||
|
||||
const classes = stateDiagram.parser.yy.getClasses();
|
||||
expect(classes.get('exampleStyleClass').styles.length).toBe(2);
|
||||
@@ -122,7 +117,6 @@ describe('ClassDefs and classes when parsing a State diagram', () => {
|
||||
diagram += 'a --> b:::exampleStyleClass' + '\n';
|
||||
|
||||
stateDiagram.parser.parse(diagram);
|
||||
stateDiagram.parser.yy.extract(stateDiagram.parser.yy.getRootDocV2());
|
||||
|
||||
const states = stateDiagram.parser.yy.getStates();
|
||||
const classes = stateDiagram.parser.yy.getClasses();
|
||||
@@ -141,7 +135,6 @@ describe('ClassDefs and classes when parsing a State diagram', () => {
|
||||
diagram += '[*]:::exampleStyleClass --> b\n';
|
||||
|
||||
stateDiagram.parser.parse(diagram);
|
||||
stateDiagram.parser.yy.extract(stateDiagram.parser.yy.getRootDocV2());
|
||||
|
||||
const states = stateDiagram.parser.yy.getStates();
|
||||
const classes = stateDiagram.parser.yy.getClasses();
|
||||
@@ -161,7 +154,6 @@ describe('ClassDefs and classes when parsing a State diagram', () => {
|
||||
diagram += 'class a,b exampleStyleClass';
|
||||
|
||||
stateDiagram.parser.parse(diagram);
|
||||
stateDiagram.parser.yy.extract(stateDiagram.parser.yy.getRootDocV2());
|
||||
let classes = stateDiagram.parser.yy.getClasses();
|
||||
let states = stateDiagram.parser.yy.getStates();
|
||||
|
||||
@@ -180,7 +172,6 @@ describe('ClassDefs and classes when parsing a State diagram', () => {
|
||||
diagram += 'class a,b,c, d, e exampleStyleClass';
|
||||
|
||||
stateDiagram.parser.parse(diagram);
|
||||
stateDiagram.parser.yy.extract(stateDiagram.parser.yy.getRootDocV2());
|
||||
const classes = stateDiagram.parser.yy.getClasses();
|
||||
const states = stateDiagram.parser.yy.getStates();
|
||||
|
||||
@@ -208,7 +199,6 @@ describe('ClassDefs and classes when parsing a State diagram', () => {
|
||||
diagram += '}\n';
|
||||
|
||||
stateDiagram.parser.parse(diagram);
|
||||
stateDiagram.parser.yy.extract(stateDiagram.parser.yy.getRootDocV2());
|
||||
|
||||
const states = stateDiagram.parser.yy.getStates();
|
||||
|
||||
@@ -224,7 +214,6 @@ describe('ClassDefs and classes when parsing a State diagram', () => {
|
||||
stateDiagram.parser.parse(`stateDiagram-v2
|
||||
id1
|
||||
style id1 background:#bbb`);
|
||||
stateDiagram.parser.yy.extract(stateDiagram.parser.yy.getRootDocV2());
|
||||
const data4Layout = stateDiagram.parser.yy.getData();
|
||||
|
||||
expect(data4Layout.nodes[0].cssStyles).toEqual(['background:#bbb']);
|
||||
@@ -234,7 +223,6 @@ describe('ClassDefs and classes when parsing a State diagram', () => {
|
||||
id1
|
||||
id2
|
||||
style id1,id2 background:#bbb`);
|
||||
stateDiagram.parser.yy.extract(stateDiagram.parser.yy.getRootDocV2());
|
||||
const data4Layout = stateDiagram.parser.yy.getData();
|
||||
|
||||
expect(data4Layout.nodes[0].cssStyles).toEqual(['background:#bbb']);
|
||||
@@ -247,7 +235,6 @@ describe('ClassDefs and classes when parsing a State diagram', () => {
|
||||
id2
|
||||
style id1,id2 background:#bbb, font-weight:bold, font-style:italic;`);
|
||||
|
||||
stateDiagram.parser.yy.extract(stateDiagram.parser.yy.getRootDocV2());
|
||||
const data4Layout = stateDiagram.parser.yy.getData();
|
||||
|
||||
expect(data4Layout.nodes[0].cssStyles).toEqual([
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { line, curveBasis } from 'd3';
|
||||
import idCache from './id-cache.js';
|
||||
import stateDb from './stateDb.js';
|
||||
import { StateDB } from './stateDb.js';
|
||||
import utils from '../../utils.js';
|
||||
import common from '../common/common.js';
|
||||
import { getConfig } from '../../diagram-api/diagramAPI.js';
|
||||
@@ -414,13 +414,13 @@ let edgeCount = 0;
|
||||
export const drawEdge = function (elem, path, relation) {
|
||||
const getRelationType = function (type) {
|
||||
switch (type) {
|
||||
case stateDb.relationType.AGGREGATION:
|
||||
case StateDB.relationType.AGGREGATION:
|
||||
return 'aggregation';
|
||||
case stateDb.relationType.EXTENSION:
|
||||
case StateDB.relationType.EXTENSION:
|
||||
return 'extension';
|
||||
case stateDb.relationType.COMPOSITION:
|
||||
case StateDB.relationType.COMPOSITION:
|
||||
return 'composition';
|
||||
case stateDb.relationType.DEPENDENCY:
|
||||
case StateDB.relationType.DEPENDENCY:
|
||||
return 'dependency';
|
||||
}
|
||||
};
|
||||
@@ -459,7 +459,7 @@ export const drawEdge = function (elem, path, relation) {
|
||||
|
||||
svgPath.attr(
|
||||
'marker-end',
|
||||
'url(' + url + '#' + getRelationType(stateDb.relationType.DEPENDENCY) + 'End' + ')'
|
||||
'url(' + url + '#' + getRelationType(StateDB.relationType.DEPENDENCY) + 'End' + ')'
|
||||
);
|
||||
|
||||
if (relation.title !== undefined) {
|
||||
|
||||
@@ -3,11 +3,14 @@
|
||||
*/
|
||||
|
||||
// default diagram direction
|
||||
export const DEFAULT_DIAGRAM_DIRECTION = 'LR';
|
||||
export const DEFAULT_DIAGRAM_DIRECTION = 'TB';
|
||||
|
||||
// default direction for any nested documents (composites)
|
||||
export const DEFAULT_NESTED_DOC_DIR = 'TB';
|
||||
|
||||
// parsed statement type for a direction
|
||||
export const STMT_DIRECTION = 'dir';
|
||||
|
||||
// parsed statement type for a state
|
||||
export const STMT_STATE = 'state';
|
||||
// parsed statement type for a relation
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,9 @@
|
||||
import stateDb from './stateDb.js';
|
||||
import { StateDB } from './stateDb.js';
|
||||
|
||||
describe('State Diagram stateDb', () => {
|
||||
let stateDb;
|
||||
beforeEach(() => {
|
||||
stateDb.clear();
|
||||
stateDb = new StateDB(1);
|
||||
});
|
||||
|
||||
describe('addStyleClass', () => {
|
||||
@@ -20,8 +21,9 @@ describe('State Diagram stateDb', () => {
|
||||
});
|
||||
|
||||
describe('addDescription to a state', () => {
|
||||
let stateDb;
|
||||
beforeEach(() => {
|
||||
stateDb.clear();
|
||||
stateDb = new StateDB(1);
|
||||
stateDb.addState('state1');
|
||||
});
|
||||
|
||||
@@ -73,3 +75,25 @@ describe('State Diagram stateDb', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('state db class', () => {
|
||||
let stateDb;
|
||||
beforeEach(() => {
|
||||
stateDb = new StateDB(1);
|
||||
});
|
||||
// This is to ensure that functions used in state JISON are exposed as function from StateDb
|
||||
it('should have functions used in flow JISON as own property', () => {
|
||||
const functionsUsedInParser = [
|
||||
'setRootDoc',
|
||||
'trimColon',
|
||||
'getDividerId',
|
||||
'setAccTitle',
|
||||
'setAccDescription',
|
||||
'setDirection',
|
||||
];
|
||||
|
||||
for (const fun of functionsUsedInParser) {
|
||||
expect(Object.hasOwn(stateDb, fun)).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { parser } from './parser/stateDiagram.jison';
|
||||
import stateDb from './stateDb.js';
|
||||
import stateDiagram from './parser/stateDiagram.jison';
|
||||
import stateDiagram, { parser } from './parser/stateDiagram.jison';
|
||||
import { DEFAULT_DIAGRAM_DIRECTION } from './stateCommon.js';
|
||||
import { StateDB } from './stateDb.js';
|
||||
|
||||
describe('state diagram V2, ', function () {
|
||||
// TODO - these examples should be put into ./parser/stateDiagram.spec.js
|
||||
describe('when parsing an info graph it', function () {
|
||||
let stateDb;
|
||||
beforeEach(function () {
|
||||
stateDb = new StateDB(2);
|
||||
parser.yy = stateDb;
|
||||
stateDiagram.parser.yy = stateDb;
|
||||
stateDiagram.parser.yy.clear();
|
||||
@@ -127,7 +129,6 @@ describe('state diagram V2, ', function () {
|
||||
`;
|
||||
|
||||
stateDiagram.parser.parse(diagram);
|
||||
stateDiagram.parser.yy.extract(stateDiagram.parser.yy.getRootDocV2());
|
||||
|
||||
const rels = stateDb.getRelations();
|
||||
const rel_1_2 = rels.find((rel) => rel.id1 === 'State1' && rel.id2 === 'State2');
|
||||
@@ -402,7 +403,6 @@ describe('state diagram V2, ', function () {
|
||||
`;
|
||||
|
||||
stateDiagram.parser.parse(diagram);
|
||||
stateDiagram.parser.yy.extract(stateDiagram.parser.yy.getRootDocV2());
|
||||
|
||||
const states = stateDb.getStates();
|
||||
expect(states.get('Active').doc[0].id).toEqual('Idle');
|
||||
@@ -413,5 +413,34 @@ describe('state diagram V2, ', function () {
|
||||
const rel_Active_Active = rels.find((rel) => rel.id1 === 'Active' && rel.id2 === 'Active');
|
||||
expect(rel_Active_Active.relationTitle).toEqual('LOG');
|
||||
});
|
||||
|
||||
it('should check default diagram direction', () => {
|
||||
const diagram = `
|
||||
stateDiagram
|
||||
[*] --> Still
|
||||
Still --> [*]
|
||||
`;
|
||||
|
||||
parser.parse(diagram);
|
||||
|
||||
// checking default direction if no direction is specified
|
||||
const defaultDir = stateDb.getDirection();
|
||||
expect(defaultDir).toEqual(DEFAULT_DIAGRAM_DIRECTION);
|
||||
});
|
||||
|
||||
it('retrieve the diagram direction correctly', () => {
|
||||
const diagram = `
|
||||
stateDiagram
|
||||
direction LR
|
||||
[*] --> Still
|
||||
Still --> [*]
|
||||
`;
|
||||
|
||||
parser.parse(diagram);
|
||||
|
||||
//retrieve the diagram direction
|
||||
const currentDirection = stateDb.getDirection();
|
||||
expect(currentDirection).toEqual('LR');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import type { DiagramDefinition } from '../../diagram-api/types.js';
|
||||
// @ts-ignore: JISON doesn't support types
|
||||
import parser from './parser/stateDiagram.jison';
|
||||
import db from './stateDb.js';
|
||||
import { StateDB } from './stateDb.js';
|
||||
import styles from './styles.js';
|
||||
import renderer from './stateRenderer-v3-unified.js';
|
||||
|
||||
export const diagram: DiagramDefinition = {
|
||||
parser,
|
||||
db,
|
||||
get db() {
|
||||
return new StateDB(2);
|
||||
},
|
||||
renderer,
|
||||
styles,
|
||||
init: (cnf) => {
|
||||
@@ -15,6 +17,5 @@ export const diagram: DiagramDefinition = {
|
||||
cnf.state = {};
|
||||
}
|
||||
cnf.state.arrowMarkerAbsolute = cnf.arrowMarkerAbsolute;
|
||||
db.clear();
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { parser } from './parser/stateDiagram.jison';
|
||||
import stateDb from './stateDb.js';
|
||||
import { StateDB } from './stateDb.js';
|
||||
|
||||
describe('state diagram, ', function () {
|
||||
describe('when parsing an info graph it', function () {
|
||||
let stateDb;
|
||||
beforeEach(function () {
|
||||
stateDb = new StateDB(1);
|
||||
parser.yy = stateDb;
|
||||
});
|
||||
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import type { DiagramDefinition } from '../../diagram-api/types.js';
|
||||
// @ts-ignore: JISON doesn't support types
|
||||
import parser from './parser/stateDiagram.jison';
|
||||
import db from './stateDb.js';
|
||||
import { StateDB } from './stateDb.js';
|
||||
import styles from './styles.js';
|
||||
import renderer from './stateRenderer.js';
|
||||
|
||||
export const diagram: DiagramDefinition = {
|
||||
parser,
|
||||
db,
|
||||
get db() {
|
||||
return new StateDB(1);
|
||||
},
|
||||
renderer,
|
||||
styles,
|
||||
init: (cnf) => {
|
||||
@@ -15,6 +17,5 @@ export const diagram: DiagramDefinition = {
|
||||
cnf.state = {};
|
||||
}
|
||||
cnf.state.arrowMarkerAbsolute = cnf.arrowMarkerAbsolute;
|
||||
db.clear();
|
||||
},
|
||||
};
|
||||
|
||||
@@ -36,7 +36,6 @@ export const getClasses = function (
|
||||
text: string,
|
||||
diagramObj: any
|
||||
): Map<string, DiagramStyleClassDef> {
|
||||
diagramObj.db.extract(diagramObj.db.getRootDocV2());
|
||||
return diagramObj.db.getClasses();
|
||||
};
|
||||
|
||||
|
||||
@@ -136,7 +136,6 @@ const renderDoc = (doc, diagram, parentId, altBkg, root, domDocument, diagObj) =
|
||||
return {};
|
||||
});
|
||||
|
||||
diagObj.db.extract(doc);
|
||||
const states = diagObj.db.getStates();
|
||||
const relations = diagObj.db.getRelations();
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
const getStyles = (options) =>
|
||||
`.label {
|
||||
font-family: 'trebuchet ms', verdana, arial, sans-serif;
|
||||
font-family: var(--mermaid-font-family);
|
||||
font-family: ${options.fontFamily};
|
||||
color: ${options.textColor};
|
||||
}
|
||||
.mouth {
|
||||
@@ -14,6 +13,7 @@ const getStyles = (options) =>
|
||||
|
||||
.legend {
|
||||
fill: ${options.textColor};
|
||||
font-family: ${options.fontFamily};
|
||||
}
|
||||
|
||||
.label text {
|
||||
@@ -79,8 +79,7 @@ const getStyles = (options) =>
|
||||
text-align: center;
|
||||
max-width: 200px;
|
||||
padding: 2px;
|
||||
font-family: 'trebuchet ms', verdana, arial, sans-serif;
|
||||
font-family: var(--mermaid-font-family);
|
||||
font-family: ${options.fontFamily};
|
||||
font-size: 12px;
|
||||
background: ${options.tertiaryColor};
|
||||
border: 1px solid ${options.border2};
|
||||
|
||||
@@ -9,7 +9,7 @@ const parserFnConstructor = (str: string) => {
|
||||
};
|
||||
};
|
||||
|
||||
const mockDB: Record<string, Mock<any, any>> = {
|
||||
const mockDB: Record<string, Mock<any>> = {
|
||||
setOrientation: vi.fn(),
|
||||
setDiagramTitle: vi.fn(),
|
||||
setXAxisTitle: vi.fn(),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { MarkdownOptions } from 'vitepress';
|
||||
import { defineConfig } from 'vitepress';
|
||||
import { version } from '../../../package.json';
|
||||
import packageJson from '../../../package.json' assert { type: 'json' };
|
||||
import MermaidExample from './mermaid-markdown-all.js';
|
||||
|
||||
const allMarkdownTransformers: MarkdownOptions = {
|
||||
@@ -95,7 +95,7 @@ function nav() {
|
||||
activeMatch: '/announcements',
|
||||
},
|
||||
{
|
||||
text: version,
|
||||
text: packageJson.version,
|
||||
items: [
|
||||
{
|
||||
text: 'Changelog',
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# Frequently Asked Questions
|
||||
|
||||
1. [How to add title to flowchart?](https://github.com/knsv/mermaid/issues/556#issuecomment-363182217)
|
||||
1. [How to add title to flowchart?](https://github.com/mermaid-js/mermaid/issues/556#issuecomment-363182217)
|
||||
1. [How to specify custom CSS file?](https://github.com/mermaidjs/mermaid.cli/pull/24#issuecomment-373402785)
|
||||
1. [How to fix tooltip misplacement issue?](https://github.com/knsv/mermaid/issues/542#issuecomment-3343564621)
|
||||
1. [How to specify gantt diagram xAxis format?](https://github.com/knsv/mermaid/issues/269#issuecomment-373229136)
|
||||
1. [How to bind an event?](https://github.com/knsv/mermaid/issues/372)
|
||||
1. [How to add newline in the text?](https://github.com/knsv/mermaid/issues/384#issuecomment-281339381)
|
||||
1. [How to have special characters in link text?](https://github.com/knsv/mermaid/issues/407#issuecomment-329944735)
|
||||
1. [How to change Flowchart curve style?](https://github.com/knsv/mermaid/issues/580#issuecomment-373929046)
|
||||
1. [How to fix tooltip misplacement issue?](https://github.com/mermaid-js/mermaid/issues/542#issuecomment-3343564621)
|
||||
1. [How to specify gantt diagram xAxis format?](https://github.com/mermaid-js/mermaid/issues/269#issuecomment-373229136)
|
||||
1. [How to bind an event?](https://github.com/mermaid-js/mermaid/issues/372)
|
||||
1. [How to add newline in the text?](https://github.com/mermaid-js/mermaid/issues/384#issuecomment-281339381)
|
||||
1. [How to have special characters in link text?](https://github.com/mermaid-js/mermaid/issues/407#issuecomment-329944735)
|
||||
1. [How to change Flowchart curve style?](https://github.com/mermaid-js/mermaid/issues/580#issuecomment-373929046)
|
||||
1. [How to create a Flowchart end-Node that says "End"](https://github.com/mermaid-js/mermaid/issues/1444#issuecomment-639528897)
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 256 KiB |
@@ -39,6 +39,7 @@ To add an integration to this list, see the [Integrations - create page](./integ
|
||||
- [Deepdwn](https://billiam.itch.io/deepdwn) ✅
|
||||
- [Doctave](https://www.doctave.com/) ✅
|
||||
- [Mermaid in Markdown code blocks](https://docs.doctave.com/components/mermaid) ✅
|
||||
- [Forgejo](https://forgejo.org/) ✅
|
||||
- [GitBook](https://gitbook.com)
|
||||
- [Mermaid Plugin](https://github.com/JozoVilcek/gitbook-plugin-mermaid)
|
||||
- [Mermaid plugin for GitBook](https://github.com/wwformat/gitbook-plugin-mermaid-pdf)
|
||||
@@ -64,6 +65,7 @@ To add an integration to this list, see the [Integrations - create page](./integ
|
||||
- [Notion](https://notion.so) ✅
|
||||
- [Observable](https://observablehq.com/@observablehq/mermaid) ✅
|
||||
- [Obsidian](https://help.obsidian.md/Editing+and+formatting/Advanced+formatting+syntax#Diagram) ✅
|
||||
- [Outline](https://docs.getoutline.com/s/guide/doc/diagrams-KQiKoT4wzK) ✅
|
||||
- [Redmine](https://redmine.org)
|
||||
- [Mermaid Macro](https://www.redmine.org/plugins/redmine_mermaid_macro)
|
||||
- [Markdown for mermaid plugin](https://github.com/jamieh-mongolian/markdown-for-mermaid-plugin)
|
||||
@@ -94,8 +96,7 @@ Blogging frameworks and platforms
|
||||
- [Nextra](https://nextra.site/)
|
||||
- [Mermaid](https://nextra.site/docs/guide/mermaid)
|
||||
- [WordPress](https://wordpress.org)
|
||||
- [WordPress Markdown Editor](https://wordpress.org/plugins/wp-githuber-md)
|
||||
- [WP-ReliableMD](https://wordpress.org/plugins/wp-reliablemd/)
|
||||
- [MerPRess](https://wordpress.org/plugins/merpress/)
|
||||
|
||||
### CMS/ECM
|
||||
|
||||
|
||||
@@ -46,28 +46,33 @@ https://codepen.io/Ryuno-Ki/pen/LNxwgR
|
||||
|
||||
[K8s.dev blog: Improve your documentation with Mermaid.js diagrams](https://www.kubernetes.dev/blog/2021/12/01/improve-your-documentation-with-mermaid.js-diagrams/)
|
||||
|
||||
## Jupyter Integration with mermaid-js
|
||||
## Jupyter / Python Integration with mermaid-js
|
||||
|
||||
Here's an example of Python integration with mermaid-js which uses the mermaid.ink service, that displays the graph in a Jupyter notebook.
|
||||
Here's an example of Python integration with mermaid-js which uses the mermaid.ink service, that displays the graph in a Jupyter notebook and save it as _.png_ image with the stated resolution (in this example, `dpi=1200`).
|
||||
|
||||
```python
|
||||
import base64
|
||||
import io, requests
|
||||
from IPython.display import Image, display
|
||||
from PIL import Image as im
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
def mm(graph):
|
||||
graphbytes = graph.encode("utf8")
|
||||
base64_bytes = base64.urlsafe_b64encode(graphbytes)
|
||||
base64_string = base64_bytes.decode("ascii")
|
||||
display(Image(url="https://mermaid.ink/img/" + base64_string))
|
||||
img = im.open(io.BytesIO(requests.get('https://mermaid.ink/img/' + base64_string).content))
|
||||
plt.imshow(img)
|
||||
plt.axis('off') # allow to hide axis
|
||||
plt.savefig('image.png', dpi=1200)
|
||||
|
||||
mm("""
|
||||
graph LR;
|
||||
A--> B & C & D;
|
||||
B--> A & E;
|
||||
C--> A & E;
|
||||
D--> A & E;
|
||||
E--> B & C & D;
|
||||
A--> B & C & D
|
||||
B--> A & E
|
||||
C--> A & E
|
||||
D--> A & E
|
||||
E--> B & C & D
|
||||
""")
|
||||
```
|
||||
|
||||
@@ -75,4 +80,4 @@ graph LR;
|
||||
|
||||

|
||||
|
||||
<!--- cspell:ignore Elle Jaoude Neurodiverse graphbytes --->
|
||||
<!--- cspell:ignore Elle Jaoude Neurodiverse graphbytes imshow savefig --->
|
||||
|
||||
@@ -44,7 +44,7 @@ For a more detailed introduction to Mermaid and some of its more basic uses, loo
|
||||
|
||||
🌐 [CDN](https://www.jsdelivr.com/package/npm/mermaid) | 📖 [Documentation](https://mermaidjs.github.io) | 🙌 [Contribution](../community/contributing.md) | 🔌 [Plug-Ins](../ecosystem/integrations-community.md)
|
||||
|
||||
> 🖖 Keep a steady pulse: mermaid needs more Collaborators, [Read More](https://github.com/knsv/mermaid/issues/866).
|
||||
> 🖖 Keep a steady pulse: mermaid needs more Collaborators, [Read More](https://github.com/mermaid-js/mermaid/issues/866).
|
||||
|
||||
:trophy: **Mermaid was nominated and won the [JS Open Source Awards (2019)](https://osawards.com/javascript/#nominees) in the category "The most exciting use of technology"!!!**
|
||||
|
||||
@@ -208,7 +208,7 @@ A quick note from Knut Sveidqvist:
|
||||
>
|
||||
> _Thank you to [Tyler Long](https://github.com/tylerlong) who has been a collaborator since April 2017._
|
||||
>
|
||||
> _Thank you to the ever-growing list of [contributors](https://github.com/knsv/mermaid/graphs/contributors) that brought the project this far!_
|
||||
> _Thank you to the ever-growing list of [contributors](https://github.com/mermaid-js/mermaid/graphs/contributors) that brought the project this far!_
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -16,26 +16,26 @@
|
||||
"fetch-contributors": "tsx .vitepress/scripts/fetch-contributors.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mdi/font": "^7.0.0",
|
||||
"@vueuse/core": "^10.9.0",
|
||||
"@mdi/font": "^7.4.47",
|
||||
"@vueuse/core": "^12.7.0",
|
||||
"font-awesome": "^4.7.0",
|
||||
"jiti": "^1.21.0",
|
||||
"jiti": "^2.4.2",
|
||||
"mermaid": "workspace:^",
|
||||
"vue": "^3.4.21"
|
||||
"vue": "^3.4.38"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iconify-json/carbon": "^1.1.31",
|
||||
"@unocss/reset": "^0.59.0",
|
||||
"@vite-pwa/vitepress": "^0.4.0",
|
||||
"@vitejs/plugin-vue": "^5.0.0",
|
||||
"fast-glob": "^3.3.2",
|
||||
"@iconify-json/carbon": "^1.1.37",
|
||||
"@unocss/reset": "^66.0.0",
|
||||
"@vite-pwa/vitepress": "^0.5.3",
|
||||
"@vitejs/plugin-vue": "^5.0.5",
|
||||
"fast-glob": "^3.3.3",
|
||||
"https-localhost": "^4.7.1",
|
||||
"pathe": "^1.1.2",
|
||||
"unocss": "^0.59.0",
|
||||
"unplugin-vue-components": "^0.26.0",
|
||||
"vite": "^5.0.0",
|
||||
"vite-plugin-pwa": "^0.19.7",
|
||||
"vitepress": "1.1.4",
|
||||
"workbox-window": "^7.0.0"
|
||||
"pathe": "^2.0.3",
|
||||
"unocss": "^66.0.0",
|
||||
"unplugin-vue-components": "^28.4.0",
|
||||
"vite": "^6.1.1",
|
||||
"vite-plugin-pwa": "^0.21.1",
|
||||
"vitepress": "1.6.3",
|
||||
"workbox-window": "^7.3.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ Mermaid syntax for ER diagrams is compatible with PlantUML, with an extension to
|
||||
|
||||
Where:
|
||||
|
||||
- `first-entity` is the name of an entity. Names must begin with an alphabetic character or an underscore (from v10.5.0+), and may also contain digits and hyphens.
|
||||
- `first-entity` is the name of an entity. Names support any unicode characters and can include spaces if surrounded by double quotes (e.g. "name with space").
|
||||
- `relationship` describes the way that both entities inter-relate. See below.
|
||||
- `second-entity` is the name of the other entity.
|
||||
- `relationship-label` describes the relationship from the perspective of the first entity.
|
||||
@@ -71,6 +71,24 @@ This statement can be read as _a property contains one or more rooms, and a room
|
||||
|
||||
Only the `first-entity` part of a statement is mandatory. This makes it possible to show an entity with no relationships, which can be useful during iterative construction of diagrams. If any other parts of a statement are specified, then all parts are mandatory.
|
||||
|
||||
#### Unicode text
|
||||
|
||||
Entity names, relationships, and attributes all support unicode text.
|
||||
|
||||
```mermaid-example
|
||||
erDiagram
|
||||
"This ❤ Unicode"
|
||||
```
|
||||
|
||||
#### Markdown formatting
|
||||
|
||||
Markdown formatting and text is also supported.
|
||||
|
||||
```mermaid-example
|
||||
erDiagram
|
||||
"This **is** _Markdown_"
|
||||
```
|
||||
|
||||
### Relationship Syntax
|
||||
|
||||
The `relationship` part of each statement can be broken down into three sub-components:
|
||||
@@ -109,6 +127,11 @@ Cardinality is a property that describes how many elements of another entity can
|
||||
|
||||
Relationships may be classified as either _identifying_ or _non-identifying_ and these are rendered with either solid or dashed lines respectively. This is relevant when one of the entities in question can not have independent existence without the other. For example a firm that insures people to drive cars might need to store data on `NAMED-DRIVER`s. In modelling this we might start out by observing that a `CAR` can be driven by many `PERSON` instances, and a `PERSON` can drive many `CAR`s - both entities can exist without the other, so this is a non-identifying relationship that we might specify in Mermaid as: `PERSON }|..|{ CAR : "driver"`. Note the two dots in the middle of the relationship that will result in a dashed line being drawn between the two entities. But when this many-to-many relationship is resolved into two one-to-many relationships, we observe that a `NAMED-DRIVER` cannot exist without both a `PERSON` and a `CAR` - the relationships become identifying and would be specified using hyphens, which translate to a solid line:
|
||||
|
||||
| Value | Alias for |
|
||||
| :---: | :---------------: |
|
||||
| -- | _identifying_ |
|
||||
| .. | _non-identifying_ |
|
||||
|
||||
**Aliases**
|
||||
|
||||
| Value | Alias for |
|
||||
@@ -116,10 +139,16 @@ Relationships may be classified as either _identifying_ or _non-identifying_ and
|
||||
| to | _identifying_ |
|
||||
| optionally to | _non-identifying_ |
|
||||
|
||||
```mermaid
|
||||
```mermaid-example
|
||||
erDiagram
|
||||
CAR ||--o{ NAMED-DRIVER : allows
|
||||
PERSON ||--o{ NAMED-DRIVER : is
|
||||
PERSON }o..o{ NAMED-DRIVER : is
|
||||
```
|
||||
|
||||
```mermaid-example
|
||||
erDiagram
|
||||
CAR 1 to zero or more NAMED-DRIVER : allows
|
||||
PERSON many(0) optionally to 0+ NAMED-DRIVER : is
|
||||
```
|
||||
|
||||
### Attributes
|
||||
@@ -144,9 +173,9 @@ erDiagram
|
||||
|
||||
The `type` values must begin with an alphabetic character and may contain digits, hyphens, underscores, parentheses and square brackets. The `name` values follow a similar format to `type`, but may start with an asterisk as another option to indicate an attribute is a primary key. Other than that, there are no restrictions, and there is no implicit set of valid data types.
|
||||
|
||||
### Entity Name Aliases (v10.5.0+)
|
||||
### Entity Name Aliases
|
||||
|
||||
An alias can be added to an entity using square brackets. If provided, the alias will be showed in the diagram instead of the entity name.
|
||||
An alias can be added to an entity using square brackets. If provided, the alias will be showed in the diagram instead of the entity name. Alias names follow all of the same rules as entity names.
|
||||
|
||||
```mermaid-example
|
||||
erDiagram
|
||||
@@ -162,7 +191,7 @@ erDiagram
|
||||
|
||||
#### Attribute Keys and Comments
|
||||
|
||||
Attributes may also have a `key` or comment defined. Keys can be `PK`, `FK` or `UK`, for Primary Key, Foreign Key or Unique Key. To specify multiple key constraints on a single attribute, separate them with a comma (e.g., `PK, FK`). A `comment` is defined by double quotes at the end of an attribute. Comments themselves cannot have double-quote characters in them.
|
||||
Attributes may also have a `key` or comment defined. Keys can be `PK`, `FK` or `UK`, for Primary Key, Foreign Key or Unique Key (markdown formatting and unicode is not supported for keys). To specify multiple key constraints on a single attribute, separate them with a comma (e.g., `PK, FK`). A `comment` is defined by double quotes at the end of an attribute. Comments themselves cannot have double-quote characters in them.
|
||||
|
||||
```mermaid-example
|
||||
erDiagram
|
||||
@@ -188,35 +217,211 @@ erDiagram
|
||||
MANUFACTURER only one to zero or more CAR : makes
|
||||
```
|
||||
|
||||
### Other Things
|
||||
### Direction
|
||||
|
||||
- If you want the relationship label to be more than one word, you must use double quotes around the phrase
|
||||
- If you don't want a label at all on a relationship, you must use an empty double-quoted string
|
||||
- (v11.1.0+) If you want a multi-line label on a relationship, use `<br />` between the two lines (`"first line<br />second line"`)
|
||||
The direction statement declares the direction of the diagram.
|
||||
|
||||
## Styling
|
||||
This declares that the diagram is oriented from top to bottom (`TB`). This can be reversed to be oriented from bottom to top (`BT`).
|
||||
|
||||
### Config options
|
||||
```mermaid-example
|
||||
erDiagram
|
||||
direction TB
|
||||
CUSTOMER ||--o{ ORDER : places
|
||||
CUSTOMER {
|
||||
string name
|
||||
string custNumber
|
||||
string sector
|
||||
}
|
||||
ORDER ||--|{ LINE-ITEM : contains
|
||||
ORDER {
|
||||
int orderNumber
|
||||
string deliveryAddress
|
||||
}
|
||||
LINE-ITEM {
|
||||
string productCode
|
||||
int quantity
|
||||
float pricePerUnit
|
||||
}
|
||||
```
|
||||
|
||||
For simple color customization:
|
||||
This declares that the diagram is oriented from left to right (`LR`). This can be reversed to be oriented from right to left (`RL`).
|
||||
|
||||
| Name | Used as |
|
||||
| :------- | :------------------------------------------------------------------- |
|
||||
| `fill` | Background color of an entity or attribute |
|
||||
| `stroke` | Border color of an entity or attribute, line color of a relationship |
|
||||
```mermaid-example
|
||||
erDiagram
|
||||
direction LR
|
||||
CUSTOMER ||--o{ ORDER : places
|
||||
CUSTOMER {
|
||||
string name
|
||||
string custNumber
|
||||
string sector
|
||||
}
|
||||
ORDER ||--|{ LINE-ITEM : contains
|
||||
ORDER {
|
||||
int orderNumber
|
||||
string deliveryAddress
|
||||
}
|
||||
LINE-ITEM {
|
||||
string productCode
|
||||
int quantity
|
||||
float pricePerUnit
|
||||
}
|
||||
```
|
||||
|
||||
### Classes used
|
||||
Possible diagram orientations are:
|
||||
|
||||
The following CSS class selectors are available for richer styling:
|
||||
- TB - Top to bottom
|
||||
- BT - Bottom to top
|
||||
- RL - Right to left
|
||||
- LR - Left to right
|
||||
|
||||
| Selector | Description |
|
||||
| :------------------------- | :---------------------------------------------------- |
|
||||
| `.er.attributeBoxEven` | The box containing attributes on even-numbered rows |
|
||||
| `.er.attributeBoxOdd` | The box containing attributes on odd-numbered rows |
|
||||
| `.er.entityBox` | The box representing an entity |
|
||||
| `.er.entityLabel` | The label for an entity |
|
||||
| `.er.relationshipLabel` | The label for a relationship |
|
||||
| `.er.relationshipLabelBox` | The box surrounding a relationship label |
|
||||
| `.er.relationshipLine` | The line representing a relationship between entities |
|
||||
### Styling a node
|
||||
|
||||
It is possible to apply specific styles such as a thicker border or a different background color to a node.
|
||||
|
||||
```mermaid-example
|
||||
erDiagram
|
||||
id1||--||id2 : label
|
||||
style id1 fill:#f9f,stroke:#333,stroke-width:4px
|
||||
style id2 fill:#bbf,stroke:#f66,stroke-width:2px,color:#fff,stroke-dasharray: 5 5
|
||||
```
|
||||
|
||||
It is also possible to attach styles to a list of nodes in one statement:
|
||||
|
||||
```
|
||||
style nodeId1,nodeId2 styleList
|
||||
```
|
||||
|
||||
#### Classes
|
||||
|
||||
More convenient than defining the style every time is to define a class of styles and attach this class to the nodes that
|
||||
should have a different look.
|
||||
|
||||
A class definition looks like the example below:
|
||||
|
||||
```
|
||||
classDef className fill:#f9f,stroke:#333,stroke-width:4px
|
||||
```
|
||||
|
||||
It is also possible to define multiple classes in one statement:
|
||||
|
||||
```
|
||||
classDef firstClassName,secondClassName font-size:12pt
|
||||
```
|
||||
|
||||
Attachment of a class to a node is done as per below:
|
||||
|
||||
```
|
||||
class nodeId1 className
|
||||
```
|
||||
|
||||
It is also possible to attach a class to a list of nodes in one statement:
|
||||
|
||||
```
|
||||
class nodeId1,nodeId2 className
|
||||
```
|
||||
|
||||
Multiple classes can be attached at the same time as well:
|
||||
|
||||
```
|
||||
class nodeId1,nodeId2 className1,className2
|
||||
```
|
||||
|
||||
A shorter form of adding a class is to attach the classname to the node using the `:::`operator as per below:
|
||||
|
||||
```mermaid-example
|
||||
erDiagram
|
||||
direction TB
|
||||
CAR:::someclass {
|
||||
string registrationNumber
|
||||
string make
|
||||
string model
|
||||
}
|
||||
PERSON:::someclass {
|
||||
string firstName
|
||||
string lastName
|
||||
int age
|
||||
}
|
||||
HOUSE:::someclass
|
||||
|
||||
classDef someclass fill:#f96
|
||||
```
|
||||
|
||||
This form can be used when declaring relationships between entities:
|
||||
|
||||
```mermaid-example
|
||||
erDiagram
|
||||
CAR {
|
||||
string registrationNumber
|
||||
string make
|
||||
string model
|
||||
}
|
||||
PERSON {
|
||||
string firstName
|
||||
string lastName
|
||||
int age
|
||||
}
|
||||
PERSON:::foo ||--|| CAR : owns
|
||||
PERSON o{--|| HOUSE:::bar : has
|
||||
|
||||
classDef foo stroke:#f00
|
||||
classDef bar stroke:#0f0
|
||||
classDef foobar stroke:#00f
|
||||
```
|
||||
|
||||
Similar to the class statement, the shorthand syntax can also apply multiple classes at once:
|
||||
|
||||
```
|
||||
nodeId:::className1,className2
|
||||
```
|
||||
|
||||
### Default class
|
||||
|
||||
If a class is named default it will be assigned to all classes without specific class definitions.
|
||||
|
||||
```
|
||||
classDef default fill:#f9f,stroke:#333,stroke-width:4px;
|
||||
```
|
||||
|
||||
> **Note:** Custom styles from style or other class statements take priority and will overwrite the default styles. (e.g. The `default` class gives nodes a background color of pink but the `blue` class will give that node a background color of blue if applied.)
|
||||
|
||||
```mermaid-example
|
||||
erDiagram
|
||||
CAR {
|
||||
string registrationNumber
|
||||
string make
|
||||
string model
|
||||
}
|
||||
PERSON {
|
||||
string firstName
|
||||
string lastName
|
||||
int age
|
||||
}
|
||||
PERSON:::foo ||--|| CAR : owns
|
||||
PERSON o{--|| HOUSE:::bar : has
|
||||
|
||||
classDef default fill:#f9f,stroke-width:4px
|
||||
classDef foo stroke:#f00
|
||||
classDef bar stroke:#0f0
|
||||
classDef foobar stroke:#00f
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Renderer
|
||||
|
||||
The layout of the diagram is done with the renderer. The default renderer is dagre.
|
||||
|
||||
You can opt to use an alternate renderer named elk by editing the configuration. The elk renderer is better for larger and/or more complex diagrams.
|
||||
|
||||
```
|
||||
---
|
||||
config:
|
||||
layout: elk
|
||||
---
|
||||
```
|
||||
|
||||
```note
|
||||
Note that the site needs to use mermaid version 9.4+ for this to work and have this featured enabled in the lazy-loading configuration.
|
||||
```
|
||||
|
||||
<!--- cspell:locale en,en-gb --->
|
||||
|
||||
@@ -711,6 +711,67 @@ flowchart TB
|
||||
B --> D
|
||||
```
|
||||
|
||||
### Attaching an ID to Edges
|
||||
|
||||
Mermaid now supports assigning IDs to edges, similar to how IDs and metadata can be attached to nodes. This feature lays the groundwork for more advanced styling, classes, and animation capabilities on edges.
|
||||
|
||||
**Syntax:**
|
||||
|
||||
To give an edge an ID, prepend the edge syntax with the ID followed by an `@` character. For example:
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A e1@–> B
|
||||
```
|
||||
|
||||
In this example, `e1` is the ID of the edge connecting `A` to `B`. You can then use this ID in later definitions or style statements, just like with nodes.
|
||||
|
||||
### Turning an Animation On
|
||||
|
||||
Once you have assigned an ID to an edge, you can turn on animations for that edge by defining the edge’s properties:
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A e1@==> B
|
||||
e1@{ animate: true }
|
||||
```
|
||||
|
||||
This tells Mermaid that the edge `e1` should be animated.
|
||||
|
||||
### Selecting Type of Animation
|
||||
|
||||
In the initial version, two animation speeds are supported: `fast` and `slow`. Selecting a specific animation type is a shorthand for enabling animation and setting the animation speed in one go.
|
||||
|
||||
**Examples:**
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A e1@–> B
|
||||
e1@{ animation: fast }
|
||||
```
|
||||
|
||||
This is equivalent to `{ animate: true, animation: fast }`.
|
||||
|
||||
### Using classDef Statements for Animations
|
||||
|
||||
You can also animate edges by assigning a class to them and then defining animation properties in a `classDef` statement. For example:
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A e1@–> B
|
||||
classDef animate stroke-dasharray: 9,5,stroke-dashoffset: 900,animation: dash 25s linear infinite;
|
||||
class e1 animate
|
||||
```
|
||||
|
||||
In this snippet:
|
||||
|
||||
- `e1@-->` creates an edge with ID `e1`.
|
||||
- `classDef animate` defines a class named `animate` with styling and animation properties.
|
||||
- `class e1 animate` applies the `animate` class to the edge `e1`.
|
||||
|
||||
**Note on Escaping Commas:**
|
||||
When setting the `stroke-dasharray` property, remember to escape commas as `\,` since commas are used as delimiters in Mermaid’s style definitions.
|
||||
|
||||
## New arrow types
|
||||
|
||||
There are new types of arrows supported:
|
||||
@@ -1074,8 +1135,7 @@ graph LR
|
||||
```
|
||||
|
||||
For a full list of available curves, including an explanation of custom curves, refer to
|
||||
the [Shapes](https://github.com/d3/d3-shape/blob/main/README.md#curves) documentation in the
|
||||
[d3-shape](https://github.com/d3/d3-shape/) project.
|
||||
the [Shapes](https://d3js.org/d3-shape/curve) documentation in the [d3-shape](https://github.com/d3/d3-shape/) project.
|
||||
|
||||
### Styling a node
|
||||
|
||||
|
||||
@@ -390,7 +390,7 @@ mermaid.ganttConfig = {
|
||||
sectionFontSize: 24, // Font size for sections
|
||||
numberSectionStyles: 1, // The number of alternating section styles
|
||||
axisFormat: '%d/%m', // Date/time format of the axis
|
||||
tickInterval: '1 week', // Axis ticks
|
||||
tickInterval: '1week', // Axis ticks
|
||||
topAxis: true, // When this flag is set, date labels will be added to the top of the chart
|
||||
displayMode: 'compact', // Turns compact mode on
|
||||
weekday: 'sunday', // On which day a week-based interval should start
|
||||
|
||||
@@ -64,7 +64,7 @@ todo[Todo]
|
||||
|
||||
## Configuration Options
|
||||
|
||||
You can customize the Kanban diagram using a configuration block at the beginning of your markdown file. This is useful for setting global settings like a base URL for tickets. Currently there is one configuration option for kanban diagrams tacketBaseUrl. This can be set as in the the following example:
|
||||
You can customize the Kanban diagram using a configuration block at the beginning of your markdown file. This is useful for setting global settings like a base URL for tickets. Currently there is one configuration option for kanban diagrams `ticketBaseUrl`. This can be set as in the the following example:
|
||||
|
||||
```yaml
|
||||
---
|
||||
|
||||
@@ -61,6 +61,26 @@ element user_defined_name {
|
||||
}
|
||||
```
|
||||
|
||||
### Markdown Formatting
|
||||
|
||||
In places where user defined text is possible (like names, requirement text, element docref, etc.), you can:
|
||||
|
||||
- Surround the text in quotes: `"example text"`
|
||||
- Use markdown formatting inside quotes: `"**bold text** and *italics*"`
|
||||
|
||||
Example:
|
||||
|
||||
```mermaid-example
|
||||
requirementDiagram
|
||||
|
||||
requirement "__test_req__" {
|
||||
id: 1
|
||||
text: "*italicized text* **bold text**"
|
||||
risk: high
|
||||
verifymethod: test
|
||||
}
|
||||
```
|
||||
|
||||
### Relationship
|
||||
|
||||
Relationships are comprised of a source node, destination node, and relationship type.
|
||||
@@ -157,4 +177,140 @@ This example uses all features of the diagram.
|
||||
test_req <- copies - test_entity2
|
||||
```
|
||||
|
||||
## Direction
|
||||
|
||||
The diagram can be rendered in different directions using the `direction` statement. Valid values are:
|
||||
|
||||
- `TB` - Top to Bottom (default)
|
||||
- `BT` - Bottom to Top
|
||||
- `LR` - Left to Right
|
||||
- `RL` - Right to Left
|
||||
|
||||
Example:
|
||||
|
||||
```mermaid-example
|
||||
requirementDiagram
|
||||
|
||||
direction LR
|
||||
|
||||
requirement test_req {
|
||||
id: 1
|
||||
text: the test text.
|
||||
risk: high
|
||||
verifymethod: test
|
||||
}
|
||||
|
||||
element test_entity {
|
||||
type: simulation
|
||||
}
|
||||
|
||||
test_entity - satisfies -> test_req
|
||||
```
|
||||
|
||||
## Styling
|
||||
|
||||
Requirements and elements can be styled using direct styling or classes. As a rule of thumb, when applying styles or classes, it accepts a list of requirement or element names and a list of class names allowing multiple assignments at a time (The only exception is the shorthand syntax `:::` which can assign multiple classes but only to one requirement or element at a time).
|
||||
|
||||
### Direct Styling
|
||||
|
||||
Use the `style` keyword to apply CSS styles directly:
|
||||
|
||||
```mermaid-example
|
||||
requirementDiagram
|
||||
|
||||
requirement test_req {
|
||||
id: 1
|
||||
text: styling example
|
||||
risk: low
|
||||
verifymethod: test
|
||||
}
|
||||
|
||||
element test_entity {
|
||||
type: simulation
|
||||
}
|
||||
|
||||
style test_req fill:#ffa,stroke:#000, color: green
|
||||
style test_entity fill:#f9f,stroke:#333, color: blue
|
||||
```
|
||||
|
||||
### Class Definitions
|
||||
|
||||
Define reusable styles using `classDef`:
|
||||
|
||||
```mermaid-example
|
||||
requirementDiagram
|
||||
|
||||
requirement test_req {
|
||||
id: 1
|
||||
text: "class styling example"
|
||||
risk: low
|
||||
verifymethod: test
|
||||
}
|
||||
|
||||
element test_entity {
|
||||
type: simulation
|
||||
}
|
||||
|
||||
classDef important fill:#f96,stroke:#333,stroke-width:4px
|
||||
classDef test fill:#ffa,stroke:#000
|
||||
```
|
||||
|
||||
### Default class
|
||||
|
||||
If a class is named default it will be applied to all nodes. Specific styles and classes should be defined afterwards to override the applied default styling.
|
||||
|
||||
```
|
||||
classDef default fill:#f9f,stroke:#333,stroke-width:4px;
|
||||
```
|
||||
|
||||
### Applying Classes
|
||||
|
||||
Classes can be applied in two ways:
|
||||
|
||||
1. Using the `class` keyword:
|
||||
|
||||
```
|
||||
class test_req,test_entity important
|
||||
```
|
||||
|
||||
2. Using the shorthand syntax with `:::` either during the definition or afterwards:
|
||||
|
||||
```
|
||||
requirement test_req:::important {
|
||||
id: 1
|
||||
text: class styling example
|
||||
risk: low
|
||||
verifymethod: test
|
||||
}
|
||||
```
|
||||
|
||||
```
|
||||
element test_elem {
|
||||
}
|
||||
|
||||
test_elem:::myClass
|
||||
```
|
||||
|
||||
### Combined Example
|
||||
|
||||
```mermaid-example
|
||||
requirementDiagram
|
||||
|
||||
requirement test_req:::important {
|
||||
id: 1
|
||||
text: "class styling example"
|
||||
risk: low
|
||||
verifymethod: test
|
||||
}
|
||||
|
||||
element test_entity {
|
||||
type: simulation
|
||||
}
|
||||
|
||||
classDef important font-weight:bold
|
||||
|
||||
class test_entity important
|
||||
style test_entity fill:#f9f,stroke:#333
|
||||
```
|
||||
|
||||
<!--- cspell:ignore reqs --->
|
||||
|
||||
@@ -12,7 +12,7 @@ timeline
|
||||
2002 : LinkedIn
|
||||
2004 : Facebook
|
||||
: Google
|
||||
2005 : Youtube
|
||||
2005 : YouTube
|
||||
2006 : Twitter
|
||||
```
|
||||
|
||||
@@ -51,7 +51,7 @@ timeline
|
||||
title History of Social Media Platform
|
||||
2002 : LinkedIn
|
||||
2004 : Facebook : Google
|
||||
2005 : Youtube
|
||||
2005 : YouTube
|
||||
2006 : Twitter
|
||||
```
|
||||
|
||||
@@ -134,7 +134,7 @@ However, if there is no section defined, then we have two possibilities:
|
||||
title History of Social Media Platform
|
||||
2002 : LinkedIn
|
||||
2004 : Facebook : Google
|
||||
2005 : Youtube
|
||||
2005 : YouTube
|
||||
2006 : Twitter
|
||||
|
||||
```
|
||||
@@ -165,7 +165,7 @@ let us look at same example, where we have disabled the multiColor option.
|
||||
title History of Social Media Platform
|
||||
2002 : LinkedIn
|
||||
2004 : Facebook : Google
|
||||
2005 : Youtube
|
||||
2005 : YouTube
|
||||
2006 : Twitter
|
||||
|
||||
```
|
||||
@@ -193,7 +193,7 @@ Now let's override the default values for the `cScale0` to `cScale2` variables:
|
||||
title History of Social Media Platform
|
||||
2002 : LinkedIn
|
||||
2004 : Facebook : Google
|
||||
2005 : Youtube
|
||||
2005 : YouTube
|
||||
2006 : Twitter
|
||||
2007 : Tumblr
|
||||
2008 : Instagram
|
||||
@@ -226,7 +226,7 @@ Let's put them to use, and see how our sample diagram looks in different themes:
|
||||
title History of Social Media Platform
|
||||
2002 : LinkedIn
|
||||
2004 : Facebook : Google
|
||||
2005 : Youtube
|
||||
2005 : YouTube
|
||||
2006 : Twitter
|
||||
2007 : Tumblr
|
||||
2008 : Instagram
|
||||
@@ -241,7 +241,7 @@ Let's put them to use, and see how our sample diagram looks in different themes:
|
||||
title History of Social Media Platform
|
||||
2002 : LinkedIn
|
||||
2004 : Facebook : Google
|
||||
2005 : Youtube
|
||||
2005 : YouTube
|
||||
2006 : Twitter
|
||||
2007 : Tumblr
|
||||
2008 : Instagram
|
||||
@@ -256,7 +256,7 @@ Let's put them to use, and see how our sample diagram looks in different themes:
|
||||
title History of Social Media Platform
|
||||
2002 : LinkedIn
|
||||
2004 : Facebook : Google
|
||||
2005 : Youtube
|
||||
2005 : YouTube
|
||||
2006 : Twitter
|
||||
2007 : Tumblr
|
||||
2008 : Instagram
|
||||
@@ -271,7 +271,7 @@ Let's put them to use, and see how our sample diagram looks in different themes:
|
||||
title History of Social Media Platform
|
||||
2002 : LinkedIn
|
||||
2004 : Facebook : Google
|
||||
2005 : Youtube
|
||||
2005 : YouTube
|
||||
2006 : Twitter
|
||||
2007 : Tumblr
|
||||
2008 : Instagram
|
||||
@@ -286,7 +286,7 @@ Let's put them to use, and see how our sample diagram looks in different themes:
|
||||
title History of Social Media Platform
|
||||
2002 : LinkedIn
|
||||
2004 : Facebook : Google
|
||||
2005 : Youtube
|
||||
2005 : YouTube
|
||||
2006 : Twitter
|
||||
2007 : Tumblr
|
||||
2008 : Instagram
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { assert, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
// -------------------------------------
|
||||
// Mocks and mocking
|
||||
@@ -67,8 +67,12 @@ vi.mock('stylis', () => {
|
||||
|
||||
import { compile, serialize } from 'stylis';
|
||||
import { Diagram } from './Diagram.js';
|
||||
import { ClassDB } from './diagrams/class/classDb.js';
|
||||
import { FlowDB } from './diagrams/flowchart/flowDb.js';
|
||||
import { SequenceDB } from './diagrams/sequence/sequenceDb.js';
|
||||
import { decodeEntities, encodeEntities } from './utils.js';
|
||||
import { toBase64 } from './utils/base64.js';
|
||||
import { StateDB } from './diagrams/state/stateDb.js';
|
||||
|
||||
/**
|
||||
* @see https://vitest.dev/guide/mocking.html Mock part of a module
|
||||
@@ -832,5 +836,108 @@ graph TD;A--x|text including URL space|B;`)
|
||||
expect(diagram).toBeInstanceOf(Diagram);
|
||||
expect(diagram.type).toBe('flowchart-v2');
|
||||
});
|
||||
|
||||
it('should not modify db when rendering different diagrams', async () => {
|
||||
const stateDiagram1 = await mermaidAPI.getDiagramFromText(
|
||||
`stateDiagram
|
||||
direction LR
|
||||
[*] --> Still
|
||||
Still --> [*]
|
||||
Still --> Moving
|
||||
Moving --> Still
|
||||
Moving --> Crash
|
||||
Crash --> [*]`
|
||||
);
|
||||
const stateDiagram2 = await mermaidAPI.getDiagramFromText(
|
||||
`stateDiagram
|
||||
direction TB
|
||||
[*] --> Still
|
||||
Still --> [*]
|
||||
Still --> Moving
|
||||
Moving --> Still
|
||||
Moving --> Crash
|
||||
Crash --> [*]`
|
||||
);
|
||||
expect(stateDiagram1.db).not.toBe(stateDiagram2.db);
|
||||
assert(stateDiagram1.db instanceof StateDB);
|
||||
assert(stateDiagram2.db instanceof StateDB);
|
||||
expect(stateDiagram1.db.getDirection()).not.toEqual(stateDiagram2.db.getDirection());
|
||||
|
||||
const flowDiagram1 = await mermaidAPI.getDiagramFromText(
|
||||
`flowchart LR
|
||||
A -- text --> B -- text2 --> C`
|
||||
);
|
||||
const flowDiagram2 = await mermaidAPI.getDiagramFromText(
|
||||
`flowchart TD
|
||||
A -- text --> B -- text2 --> C`
|
||||
);
|
||||
// Since flowDiagram will return new Db object each time, we can compare the db to be different.
|
||||
expect(flowDiagram1.db).not.toBe(flowDiagram2.db);
|
||||
assert(flowDiagram1.db instanceof FlowDB);
|
||||
assert(flowDiagram2.db instanceof FlowDB);
|
||||
expect(flowDiagram1.db.getDirection()).not.toEqual(flowDiagram2.db.getDirection());
|
||||
|
||||
const classDiagram1 = await mermaidAPI.getDiagramFromText(
|
||||
`classDiagram
|
||||
direction TB
|
||||
class Student {
|
||||
-idCard : IdCard
|
||||
}
|
||||
class IdCard{
|
||||
-id : int
|
||||
-name : string
|
||||
}
|
||||
class Bike{
|
||||
-id : int
|
||||
-name : string
|
||||
}
|
||||
Student "1" --o "1" IdCard : carries
|
||||
Student "1" --o "1" Bike : rides`
|
||||
);
|
||||
const classDiagram2 = await mermaidAPI.getDiagramFromText(
|
||||
`classDiagram
|
||||
direction LR
|
||||
class Student {
|
||||
-idCard : IdCard
|
||||
}
|
||||
class IdCard{
|
||||
-id : int
|
||||
-name : string
|
||||
}
|
||||
class Bike{
|
||||
-id : int
|
||||
-name : string
|
||||
}
|
||||
Student "1" --o "1" IdCard : carries
|
||||
Student "1" --o "1" Bike : rides`
|
||||
);
|
||||
// Since classDiagram will return new Db object each time, we can compare the db to be different.
|
||||
expect(classDiagram1.db).not.toBe(classDiagram2.db);
|
||||
assert(classDiagram1.db instanceof ClassDB);
|
||||
assert(classDiagram2.db instanceof ClassDB);
|
||||
expect(classDiagram1.db.getDirection()).not.toEqual(classDiagram2.db.getDirection());
|
||||
|
||||
const sequenceDiagram1 = await mermaidAPI.getDiagramFromText(
|
||||
`sequenceDiagram
|
||||
Alice->>+John: Hello John, how are you?
|
||||
Alice->>+John: John, can you hear me?
|
||||
John-->>-Alice: Hi Alice, I can hear you!
|
||||
John-->>-Alice: I feel great!`
|
||||
);
|
||||
const sequenceDiagram2 = await mermaidAPI.getDiagramFromText(
|
||||
`sequenceDiagram
|
||||
actor A1
|
||||
Alice->>+John: Hello John, how are you?
|
||||
Alice->>+John: John, can you hear me?
|
||||
John-->>-Alice: Hi Alice, I can hear you!
|
||||
John-->>-Alice: I feel great!`
|
||||
);
|
||||
|
||||
// Since sequenceDiagram will return new Db object each time, we can compare the db to be different.
|
||||
expect(sequenceDiagram1.db).not.toBe(sequenceDiagram2.db);
|
||||
assert(sequenceDiagram1.db instanceof SequenceDB);
|
||||
assert(sequenceDiagram2.db instanceof SequenceDB);
|
||||
expect(sequenceDiagram1.db.getActors()).not.toEqual(sequenceDiagram2.db.getActors());
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,7 +8,7 @@ import { compile, serialize, stringify } from 'stylis';
|
||||
// @ts-ignore: TODO Fix ts errors
|
||||
import DOMPurify from 'dompurify';
|
||||
import isEmpty from 'lodash-es/isEmpty.js';
|
||||
import { version } from '../package.json';
|
||||
import packageJson from '../package.json' assert { type: 'json' };
|
||||
import { addSVGa11yTitleDescription, setA11yDiagramInfo } from './accessibility.js';
|
||||
import assignWithDepth from './assignWithDepth.js';
|
||||
import * as configApi from './config.js';
|
||||
@@ -422,12 +422,12 @@ const render = async function (
|
||||
// -------------------------------------------------------------------------------
|
||||
// Draw the diagram with the renderer
|
||||
try {
|
||||
await diag.renderer.draw(text, id, version, diag);
|
||||
await diag.renderer.draw(text, id, packageJson.version, diag);
|
||||
} catch (e) {
|
||||
if (config.suppressErrorRendering) {
|
||||
removeTempElements();
|
||||
} else {
|
||||
errorRenderer.draw(text, id, version);
|
||||
errorRenderer.draw(text, id, packageJson.version);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
@@ -346,6 +346,7 @@ export const render = async (data4Layout, svg) => {
|
||||
edge1.label = '';
|
||||
edge1.arrowTypeEnd = 'none';
|
||||
edge1.id = nodeId + '-cyclic-special-1';
|
||||
edgeMid.arrowTypeStart = 'none';
|
||||
edgeMid.arrowTypeEnd = 'none';
|
||||
edgeMid.id = nodeId + '-cyclic-special-mid';
|
||||
edge2.label = '';
|
||||
@@ -354,6 +355,7 @@ export const render = async (data4Layout, svg) => {
|
||||
edge2.toCluster = nodeId;
|
||||
}
|
||||
edge2.id = nodeId + '-cyclic-special-2';
|
||||
edge2.arrowTypeStart = 'none';
|
||||
graph.setEdge(nodeId, specialId1, edge1, nodeId + '-cyclic-special-0');
|
||||
graph.setEdge(specialId1, specialId2, edgeMid, nodeId + '-cyclic-special-1');
|
||||
graph.setEdge(specialId2, nodeId, edge2, nodeId + '-cyc<lic-special-2');
|
||||
|
||||
@@ -15,26 +15,33 @@ export const addEdgeMarkers = (
|
||||
edge: Pick<EdgeData, 'arrowTypeStart' | 'arrowTypeEnd'>,
|
||||
url: string,
|
||||
id: string,
|
||||
diagramType: string
|
||||
diagramType: string,
|
||||
strokeColor?: string
|
||||
) => {
|
||||
if (edge.arrowTypeStart) {
|
||||
addEdgeMarker(svgPath, 'start', edge.arrowTypeStart, url, id, diagramType);
|
||||
addEdgeMarker(svgPath, 'start', edge.arrowTypeStart, url, id, diagramType, strokeColor);
|
||||
}
|
||||
if (edge.arrowTypeEnd) {
|
||||
addEdgeMarker(svgPath, 'end', edge.arrowTypeEnd, url, id, diagramType);
|
||||
addEdgeMarker(svgPath, 'end', edge.arrowTypeEnd, url, id, diagramType, strokeColor);
|
||||
}
|
||||
};
|
||||
|
||||
const arrowTypesMap = {
|
||||
arrow_cross: 'cross',
|
||||
arrow_point: 'point',
|
||||
arrow_barb: 'barb',
|
||||
arrow_circle: 'circle',
|
||||
aggregation: 'aggregation',
|
||||
extension: 'extension',
|
||||
composition: 'composition',
|
||||
dependency: 'dependency',
|
||||
lollipop: 'lollipop',
|
||||
arrow_cross: { type: 'cross', fill: false },
|
||||
arrow_point: { type: 'point', fill: true },
|
||||
arrow_barb: { type: 'barb', fill: true },
|
||||
arrow_circle: { type: 'circle', fill: false },
|
||||
aggregation: { type: 'aggregation', fill: false },
|
||||
extension: { type: 'extension', fill: false },
|
||||
composition: { type: 'composition', fill: true },
|
||||
dependency: { type: 'dependency', fill: true },
|
||||
lollipop: { type: 'lollipop', fill: false },
|
||||
only_one: { type: 'onlyOne', fill: false },
|
||||
zero_or_one: { type: 'zeroOrOne', fill: false },
|
||||
one_or_more: { type: 'oneOrMore', fill: false },
|
||||
zero_or_more: { type: 'zeroOrMore', fill: false },
|
||||
requirement_arrow: { type: 'requirement_arrow', fill: false },
|
||||
requirement_contains: { type: 'requirement_contains', fill: false },
|
||||
} as const;
|
||||
|
||||
const addEdgeMarker = (
|
||||
@@ -43,15 +50,55 @@ const addEdgeMarker = (
|
||||
arrowType: string,
|
||||
url: string,
|
||||
id: string,
|
||||
diagramType: string
|
||||
diagramType: string,
|
||||
strokeColor?: string
|
||||
) => {
|
||||
const endMarkerType = arrowTypesMap[arrowType as keyof typeof arrowTypesMap];
|
||||
const arrowTypeInfo = arrowTypesMap[arrowType as keyof typeof arrowTypesMap];
|
||||
|
||||
if (!endMarkerType) {
|
||||
if (!arrowTypeInfo) {
|
||||
log.warn(`Unknown arrow type: ${arrowType}`);
|
||||
return; // unknown arrow type, ignore
|
||||
}
|
||||
|
||||
const endMarkerType = arrowTypeInfo.type;
|
||||
const suffix = position === 'start' ? 'Start' : 'End';
|
||||
svgPath.attr(`marker-${position}`, `url(${url}#${id}_${diagramType}-${endMarkerType}${suffix})`);
|
||||
const originalMarkerId = `${id}_${diagramType}-${endMarkerType}${suffix}`;
|
||||
|
||||
// If stroke color is specified and non-empty, create or use a colored variant of the marker
|
||||
if (strokeColor && strokeColor.trim() !== '') {
|
||||
// Create a sanitized color value for use in IDs
|
||||
const colorId = strokeColor.replace(/[^\dA-Za-z]/g, '_');
|
||||
const coloredMarkerId = `${originalMarkerId}_${colorId}`;
|
||||
|
||||
// Check if the colored marker already exists
|
||||
if (!document.getElementById(coloredMarkerId)) {
|
||||
// Get the original marker
|
||||
const originalMarker = document.getElementById(originalMarkerId);
|
||||
if (originalMarker) {
|
||||
// Clone the marker and create colored version
|
||||
const coloredMarker = originalMarker.cloneNode(true) as Element;
|
||||
coloredMarker.id = coloredMarkerId;
|
||||
|
||||
// Apply colors to the paths inside the marker
|
||||
const paths = coloredMarker.querySelectorAll('path, circle, line');
|
||||
paths.forEach((path) => {
|
||||
path.setAttribute('stroke', strokeColor);
|
||||
|
||||
// Apply fill only to markers that should be filled
|
||||
if (arrowTypeInfo.fill) {
|
||||
path.setAttribute('fill', strokeColor);
|
||||
}
|
||||
});
|
||||
|
||||
// Add the new colored marker to the defs section
|
||||
originalMarker.parentNode?.appendChild(coloredMarker);
|
||||
}
|
||||
}
|
||||
|
||||
// Use the colored marker
|
||||
svgPath.attr(`marker-${position}`, `url(${url}#${coloredMarkerId})`);
|
||||
} else {
|
||||
// Always use the original marker for unstyled edges
|
||||
svgPath.attr(`marker-${position}`, `url(${url}#${originalMarkerId})`);
|
||||
}
|
||||
};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user