diff --git a/packages/mermaid/src/config.type.ts b/packages/mermaid/src/config.type.ts index 035a158e0..cd8401d7c 100644 --- a/packages/mermaid/src/config.type.ts +++ b/packages/mermaid/src/config.type.ts @@ -793,6 +793,8 @@ export interface ErDiagramConfig extends BaseDiagramConfig { * */ entityPadding?: number; + nodeSpacing?: number; + rankSpacing?: number; /** * Stroke color of box edges and lines. */ diff --git a/packages/mermaid/src/diagrams/er/erDb.js b/packages/mermaid/src/diagrams/er/erDb.js deleted file mode 100644 index f24f48198..000000000 --- a/packages/mermaid/src/diagrams/er/erDb.js +++ /dev/null @@ -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, -}; diff --git a/packages/mermaid/src/diagrams/er/erDb.ts b/packages/mermaid/src/diagrams/er/erDb.ts new file mode 100644 index 000000000..c8ea3136c --- /dev/null +++ b/packages/mermaid/src/diagrams/er/erDb.ts @@ -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(); +let relationships: Relationship[] = []; +let classes = new Map(); +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, +}; diff --git a/packages/mermaid/src/diagrams/er/erDiagram.ts b/packages/mermaid/src/diagrams/er/erDiagram.ts index adfa525fc..1647f181b 100644 --- a/packages/mermaid/src/diagrams/er/erDiagram.ts +++ b/packages/mermaid/src/diagrams/er/erDiagram.ts @@ -1,7 +1,7 @@ // @ts-ignore: TODO: Fix ts errors import erParser from './parser/erDiagram.jison'; import erDb from './erDb.js'; -import erRenderer from './erRenderer.js'; +import erRenderer from './erRenderer-unified.js'; import erStyles from './styles.js'; export const diagram = { diff --git a/packages/mermaid/src/diagrams/er/erRenderer-unified.ts b/packages/mermaid/src/diagrams/er/erRenderer-unified.ts new file mode 100644 index 000000000..be668df69 --- /dev/null +++ b/packages/mermaid/src/diagrams/er/erRenderer-unified.ts @@ -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, +}; diff --git a/packages/mermaid/src/diagrams/er/erTypes.ts b/packages/mermaid/src/diagrams/er/erTypes.ts new file mode 100644 index 000000000..7670302f0 --- /dev/null +++ b/packages/mermaid/src/diagrams/er/erTypes.ts @@ -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[]; +} diff --git a/packages/mermaid/src/diagrams/er/styles.js b/packages/mermaid/src/diagrams/er/styles.js index 08ea2e851..45a9dd7ec 100644 --- a/packages/mermaid/src/diagrams/er/styles.js +++ b/packages/mermaid/src/diagrams/er/styles.js @@ -24,9 +24,36 @@ const getStyles = (options) => } } - .relationshipLine { - stroke: ${options.lineColor}; - } + .edgeLabel .label { + 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 { text-anchor: middle; diff --git a/packages/mermaid/src/schemas/config.schema.yaml b/packages/mermaid/src/schemas/config.schema.yaml index a7b3549eb..980561084 100644 --- a/packages/mermaid/src/schemas/config.schema.yaml +++ b/packages/mermaid/src/schemas/config.schema.yaml @@ -1280,6 +1280,14 @@ $defs: # JSON Schema definition (maybe we should move these to a separate file) type: integer default: 15 minimum: 0 + nodeSpacing: + type: integer + default: 140 + minimum: 0 + rankSpacing: + type: integer + default: 80 + minimum: 0 stroke: description: Stroke color of box edges and lines. type: string