mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-08-15 06:19:24 +02:00
Initial update to new renderer
This commit is contained in:
@@ -793,6 +793,8 @@ export interface ErDiagramConfig extends BaseDiagramConfig {
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
entityPadding?: number;
|
entityPadding?: number;
|
||||||
|
nodeSpacing?: number;
|
||||||
|
rankSpacing?: number;
|
||||||
/**
|
/**
|
||||||
* Stroke color of box edges and lines.
|
* Stroke color of box edges and lines.
|
||||||
*/
|
*/
|
||||||
|
@@ -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,
|
|
||||||
};
|
|
240
packages/mermaid/src/diagrams/er/erDb.ts
Normal file
240
packages/mermaid/src/diagrams/er/erDb.ts
Normal file
@@ -0,0 +1,240 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
let entities = new Map<string, EntityNode>();
|
||||||
|
let relationships: Relationship[] = [];
|
||||||
|
let classes = new Map<string, EntityClass>();
|
||||||
|
let direction = 'TB';
|
||||||
|
|
||||||
|
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 name - The name of the entity
|
||||||
|
* @param alias - The alias of the entity
|
||||||
|
*/
|
||||||
|
const addEntity = function (name: string, alias = ''): EntityNode {
|
||||||
|
if (!entities.has(name)) {
|
||||||
|
entities.set(name, {
|
||||||
|
id: `entity-${name}-${entities.size}`,
|
||||||
|
label: name,
|
||||||
|
attributes: [],
|
||||||
|
alias,
|
||||||
|
shape: 'erBox',
|
||||||
|
look: getConfig().look || 'default',
|
||||||
|
cssClasses: [],
|
||||||
|
cssStyles: [],
|
||||||
|
});
|
||||||
|
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 getEntity = function (name: string) {
|
||||||
|
return entities.get(name);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getEntities = () => entities;
|
||||||
|
|
||||||
|
const getClasses = () => classes;
|
||||||
|
|
||||||
|
const addAttributes = function (entityName: string, attribs: Attribute[]) {
|
||||||
|
const 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--) {
|
||||||
|
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
|
||||||
|
*/
|
||||||
|
const addRelationship = function (entA: string, rolA: string, entB: string, rSpec: RelSpec) {
|
||||||
|
const entityA = entities.get(entA);
|
||||||
|
const entityB = entities.get(entB);
|
||||||
|
if (!entityA || !entityB) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rel = {
|
||||||
|
entityA: entityA.id,
|
||||||
|
roleA: rolA,
|
||||||
|
entityB: entityB.id,
|
||||||
|
relSpec: rSpec,
|
||||||
|
};
|
||||||
|
|
||||||
|
relationships.push(rel);
|
||||||
|
log.debug('Added new relationship :', rel);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getRelationships = () => relationships;
|
||||||
|
|
||||||
|
const getDirection = () => direction;
|
||||||
|
const setDirection = (dir: string) => {
|
||||||
|
direction = dir;
|
||||||
|
};
|
||||||
|
|
||||||
|
const clear = function () {
|
||||||
|
entities = new Map();
|
||||||
|
classes = new Map();
|
||||||
|
relationships = [];
|
||||||
|
commonClear();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getData = function () {
|
||||||
|
const nodes: Node[] = [];
|
||||||
|
const edges: Edge[] = [];
|
||||||
|
const config = getConfig();
|
||||||
|
|
||||||
|
for (const entityKey of entities.keys()) {
|
||||||
|
const entityNode = entities.get(entityKey);
|
||||||
|
if (entityNode) {
|
||||||
|
entityNode.cssCompiledStyles = getCompiledStyles(entityNode.cssClasses!);
|
||||||
|
nodes.push(entityNode as unknown as Node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let cnt = 0;
|
||||||
|
for (const relationship of relationships) {
|
||||||
|
const edge: Edge = {
|
||||||
|
id: getEdgeId(relationship.entityA, relationship.entityB, { prefix: 'id', counter: cnt++ }),
|
||||||
|
type: 'normal',
|
||||||
|
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' };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const addCssStyles = function (ids: string[], styles: string[]) {
|
||||||
|
for (const id of ids) {
|
||||||
|
const entity = entities.get(id);
|
||||||
|
if (!styles || !entity) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const style of styles) {
|
||||||
|
entity.cssStyles!.push(style);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const addClass = function (ids: string[], style: string[]) {
|
||||||
|
ids.forEach(function (id) {
|
||||||
|
let classNode = classes.get(id);
|
||||||
|
if (classNode === undefined) {
|
||||||
|
classNode = { id, styles: [], textStyles: [] };
|
||||||
|
classes.set(id, classNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (style) {
|
||||||
|
style.forEach(function (s) {
|
||||||
|
if (/color/.exec(s)) {
|
||||||
|
const newStyle = s.replace('fill', 'bgFill'); // .replace('color', 'fill');
|
||||||
|
classNode.textStyles.push(newStyle);
|
||||||
|
}
|
||||||
|
classNode.styles.push(s);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setClass = function (ids: string[], classNames: string[]) {
|
||||||
|
for (const id of ids) {
|
||||||
|
const entity = entities.get(id);
|
||||||
|
if (entity) {
|
||||||
|
for (const className of classNames) {
|
||||||
|
entity.cssClasses!.push(className);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function getCompiledStyles(classDefs: string[]) {
|
||||||
|
let compiledStyles: string[] = [];
|
||||||
|
for (const customClass of classDefs) {
|
||||||
|
const cssClass = 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
Cardinality,
|
||||||
|
Identification,
|
||||||
|
getConfig: () => getConfig().er,
|
||||||
|
addEntity,
|
||||||
|
addAttributes,
|
||||||
|
getEntities,
|
||||||
|
getEntity,
|
||||||
|
getClasses,
|
||||||
|
addRelationship,
|
||||||
|
getRelationships,
|
||||||
|
clear,
|
||||||
|
getDirection,
|
||||||
|
setDirection,
|
||||||
|
setAccTitle,
|
||||||
|
getAccTitle,
|
||||||
|
setAccDescription,
|
||||||
|
getAccDescription,
|
||||||
|
setDiagramTitle,
|
||||||
|
getDiagramTitle,
|
||||||
|
getData,
|
||||||
|
addCssStyles,
|
||||||
|
addClass,
|
||||||
|
setClass,
|
||||||
|
};
|
@@ -1,7 +1,7 @@
|
|||||||
// @ts-ignore: TODO: Fix ts errors
|
// @ts-ignore: TODO: Fix ts errors
|
||||||
import erParser from './parser/erDiagram.jison';
|
import erParser from './parser/erDiagram.jison';
|
||||||
import erDb from './erDb.js';
|
import erDb from './erDb.js';
|
||||||
import erRenderer from './erRenderer.js';
|
import erRenderer from './erRenderer-unified.js';
|
||||||
import erStyles from './styles.js';
|
import erStyles from './styles.js';
|
||||||
|
|
||||||
export const diagram = {
|
export const diagram = {
|
||||||
|
46
packages/mermaid/src/diagrams/er/erRenderer-unified.ts
Normal file
46
packages/mermaid/src/diagrams/er/erRenderer-unified.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
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 db from './erDb.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 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 = db.getDirection();
|
||||||
|
|
||||||
|
data4Layout.markers = ['only_one', 'zero_or_one', 'one_or_more', 'zero_or_more'];
|
||||||
|
data4Layout.diagramId = id;
|
||||||
|
await render(data4Layout, svg);
|
||||||
|
const padding = 8;
|
||||||
|
utils.insertTitle(
|
||||||
|
svg,
|
||||||
|
'erDiagramTitleText',
|
||||||
|
conf?.titleTopMargin ?? 25,
|
||||||
|
diag.db.getDiagramTitle()
|
||||||
|
);
|
||||||
|
|
||||||
|
setupViewPortForSVG(svg, padding, 'erDiagram', conf?.useMaxWidth ?? true);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
draw,
|
||||||
|
};
|
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[];
|
||||||
|
}
|
@@ -24,9 +24,36 @@ const getStyles = (options) =>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.relationshipLine {
|
.edgeLabel .label {
|
||||||
stroke: ${options.lineColor};
|
fill: ${options.nodeBorder};
|
||||||
}
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edgeLabel .label .labelBkg {
|
||||||
|
background: ${options.mainBkg};
|
||||||
|
}
|
||||||
|
|
||||||
|
.node rect,
|
||||||
|
.node circle,
|
||||||
|
.node ellipse,
|
||||||
|
.node polygon,
|
||||||
|
.node path {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
.entityTitleText {
|
.entityTitleText {
|
||||||
text-anchor: middle;
|
text-anchor: middle;
|
||||||
|
@@ -1280,6 +1280,14 @@ $defs: # JSON Schema definition (maybe we should move these to a separate file)
|
|||||||
type: integer
|
type: integer
|
||||||
default: 15
|
default: 15
|
||||||
minimum: 0
|
minimum: 0
|
||||||
|
nodeSpacing:
|
||||||
|
type: integer
|
||||||
|
default: 140
|
||||||
|
minimum: 0
|
||||||
|
rankSpacing:
|
||||||
|
type: integer
|
||||||
|
default: 80
|
||||||
|
minimum: 0
|
||||||
stroke:
|
stroke:
|
||||||
description: Stroke color of box edges and lines.
|
description: Stroke color of box edges and lines.
|
||||||
type: string
|
type: string
|
||||||
|
Reference in New Issue
Block a user