diff --git a/packages/mermaid/src/diagrams/architecture/architecture.spec.ts b/packages/mermaid/src/diagrams/architecture/architecture.spec.ts index d0405d025..49cc18a07 100644 --- a/packages/mermaid/src/diagrams/architecture/architecture.spec.ts +++ b/packages/mermaid/src/diagrams/architecture/architecture.spec.ts @@ -1,21 +1,12 @@ import { it, describe, expect } from 'vitest'; -import { db } from './architectureDb.js'; import { parser } from './architectureParser.js'; - -const { - clear, - getDiagramTitle, - getAccTitle, - getAccDescription, - getServices, - getGroups, - getEdges, - getJunctions, -} = db; - +import { ArchitectureDB } from './architectureDb.js'; describe('architecture diagrams', () => { + let db: ArchitectureDB; beforeEach(() => { - clear(); + db = new ArchitectureDB(); + // @ts-expect-error since type is set to undefined we will have error + parser.parser?.yy = db; }); describe('architecture diagram definitions', () => { @@ -36,7 +27,7 @@ describe('architecture diagrams', () => { it('should handle title on the first line', async () => { const str = `architecture-beta title Simple Architecture Diagram`; await expect(parser.parse(str)).resolves.not.toThrow(); - expect(getDiagramTitle()).toBe('Simple Architecture Diagram'); + expect(db.getDiagramTitle()).toBe('Simple Architecture Diagram'); }); it('should handle title on another line', async () => { @@ -44,7 +35,7 @@ describe('architecture diagrams', () => { title Simple Architecture Diagram `; await expect(parser.parse(str)).resolves.not.toThrow(); - expect(getDiagramTitle()).toBe('Simple Architecture Diagram'); + expect(db.getDiagramTitle()).toBe('Simple Architecture Diagram'); }); it('should handle accessibility title and description', async () => { @@ -53,8 +44,8 @@ describe('architecture diagrams', () => { accDescr: Accessibility Description `; await expect(parser.parse(str)).resolves.not.toThrow(); - expect(getAccTitle()).toBe('Accessibility Title'); - expect(getAccDescription()).toBe('Accessibility Description'); + expect(db.getAccTitle()).toBe('Accessibility Title'); + expect(db.getAccDescription()).toBe('Accessibility Description'); }); it('should handle multiline accessibility description', async () => { @@ -64,7 +55,7 @@ describe('architecture diagrams', () => { } `; await expect(parser.parse(str)).resolves.not.toThrow(); - expect(getAccDescription()).toBe('Accessibility Description'); + expect(db.getAccDescription()).toBe('Accessibility Description'); }); }); }); diff --git a/packages/mermaid/src/diagrams/architecture/architectureDb.ts b/packages/mermaid/src/diagrams/architecture/architectureDb.ts index c7bd64e21..5d90bd8a6 100644 --- a/packages/mermaid/src/diagrams/architecture/architectureDb.ts +++ b/packages/mermaid/src/diagrams/architecture/architectureDb.ts @@ -1,8 +1,9 @@ +import { getConfig as commonGetConfig } from '../../config.js'; import type { ArchitectureDiagramConfig } from '../../config.type.js'; import DEFAULT_CONFIG from '../../defaultConfig.js'; -import { getConfig as commonGetConfig } from '../../config.js'; +import type { DiagramDB } from '../../diagram-api/types.js'; import type { D3Element } from '../../types.js'; -import { ImperativeState } from '../../utils/imperativeState.js'; +import { cleanAndMerge } from '../../utils.js'; import { clear as commonClear, getAccDescription, @@ -14,7 +15,6 @@ import { } from '../common/commonDb.js'; import type { ArchitectureAlignment, - ArchitectureDB, ArchitectureDirectionPair, ArchitectureDirectionPairMap, ArchitectureEdge, @@ -33,330 +33,334 @@ import { isArchitectureService, shiftPositionByArchitectureDirectionPair, } from './architectureTypes.js'; -import { cleanAndMerge } from '../../utils.js'; const DEFAULT_ARCHITECTURE_CONFIG: Required = DEFAULT_CONFIG.architecture; +export class ArchitectureDB implements DiagramDB { + private nodes: Record = {}; + private groups: Record = {}; + private edges: ArchitectureEdge[] = []; + private registeredIds: Record = {}; + // private config: Required = DEFAULT_ARCHITECTURE_CONFIG; + private dataStructures?: ArchitectureState['dataStructures']; + private elements: Record = {}; -const state = new ImperativeState(() => ({ - nodes: {}, - groups: {}, - edges: [], - registeredIds: {}, - config: DEFAULT_ARCHITECTURE_CONFIG, - dataStructures: undefined, - elements: {}, -})); - -const clear = (): void => { - state.reset(); - commonClear(); -}; - -const addService = function ({ - id, - icon, - in: parent, - title, - iconText, -}: Omit) { - if (state.records.registeredIds[id] !== undefined) { - throw new Error( - `The service id [${id}] is already in use by another ${state.records.registeredIds[id]}` - ); - } - if (parent !== undefined) { - if (id === parent) { - throw new Error(`The service [${id}] cannot be placed within itself`); - } - if (state.records.registeredIds[parent] === undefined) { - throw new Error( - `The service [${id}]'s parent does not exist. Please make sure the parent is created before this service` - ); - } - if (state.records.registeredIds[parent] === 'node') { - throw new Error(`The service [${id}]'s parent is not a group`); - } + constructor() { + this.clear(); } - state.records.registeredIds[id] = 'node'; + public clear(): void { + this.nodes = {}; + this.groups = {}; + this.edges = []; + this.registeredIds = {}; + this.dataStructures = undefined; + this.elements = {}; + commonClear(); + } - state.records.nodes[id] = { + public addService({ id, - type: 'service', icon, + in: parent, + title, iconText, - title, - edges: [], - in: parent, - }; -}; - -const getServices = (): ArchitectureService[] => - Object.values(state.records.nodes).filter(isArchitectureService); - -const addJunction = function ({ id, in: parent }: Omit) { - state.records.registeredIds[id] = 'node'; - - state.records.nodes[id] = { - id, - type: 'junction', - edges: [], - in: parent, - }; -}; - -const getJunctions = (): ArchitectureJunction[] => - Object.values(state.records.nodes).filter(isArchitectureJunction); - -const getNodes = (): ArchitectureNode[] => Object.values(state.records.nodes); - -const getNode = (id: string): ArchitectureNode | null => state.records.nodes[id]; - -const addGroup = function ({ id, icon, in: parent, title }: ArchitectureGroup) { - if (state.records.registeredIds[id] !== undefined) { - throw new Error( - `The group id [${id}] is already in use by another ${state.records.registeredIds[id]}` - ); - } - if (parent !== undefined) { - if (id === parent) { - throw new Error(`The group [${id}] cannot be placed within itself`); - } - if (state.records.registeredIds[parent] === undefined) { + }: Omit): void { + if (this.registeredIds[id] !== undefined) { throw new Error( - `The group [${id}]'s parent does not exist. Please make sure the parent is created before this group` + `The service id [${id}] is already in use by another ${this.registeredIds[id]}` ); } - if (state.records.registeredIds[parent] === 'node') { - throw new Error(`The group [${id}]'s parent is not a group`); + if (parent !== undefined) { + if (id === parent) { + throw new Error(`The service [${id}] cannot be placed within itself`); + } + if (this.registeredIds[parent] === undefined) { + throw new Error( + `The service [${id}]'s parent does not exist. Please make sure the parent is created before this service` + ); + } + if (this.registeredIds[parent] === 'node') { + throw new Error(`The service [${id}]'s parent is not a group`); + } } + + this.registeredIds[id] = 'node'; + + this.nodes[id] = { + id, + type: 'service', + icon, + iconText, + title, + edges: [], + in: parent, + }; } - state.records.registeredIds[id] = 'group'; - - state.records.groups[id] = { - id, - icon, - title, - in: parent, - }; -}; -const getGroups = (): ArchitectureGroup[] => { - return Object.values(state.records.groups); -}; - -const addEdge = function ({ - lhsId, - rhsId, - lhsDir, - rhsDir, - lhsInto, - rhsInto, - lhsGroup, - rhsGroup, - title, -}: ArchitectureEdge) { - if (!isArchitectureDirection(lhsDir)) { - throw new Error( - `Invalid direction given for left hand side of edge ${lhsId}--${rhsId}. Expected (L,R,T,B) got ${lhsDir}` - ); - } - if (!isArchitectureDirection(rhsDir)) { - throw new Error( - `Invalid direction given for right hand side of edge ${lhsId}--${rhsId}. Expected (L,R,T,B) got ${rhsDir}` - ); + public getServices(): ArchitectureService[] { + return Object.values(this.nodes).filter(isArchitectureService); } - if (state.records.nodes[lhsId] === undefined && state.records.groups[lhsId] === undefined) { - throw new Error( - `The left-hand id [${lhsId}] does not yet exist. Please create the service/group before declaring an edge to it.` - ); - } - if (state.records.nodes[rhsId] === undefined && state.records.groups[lhsId] === undefined) { - throw new Error( - `The right-hand id [${rhsId}] does not yet exist. Please create the service/group before declaring an edge to it.` - ); + public addJunction({ id, in: parent }: Omit): void { + this.registeredIds[id] = 'node'; + + this.nodes[id] = { + id, + type: 'junction', + edges: [], + in: parent, + }; } - const lhsGroupId = state.records.nodes[lhsId].in; - const rhsGroupId = state.records.nodes[rhsId].in; - if (lhsGroup && lhsGroupId && rhsGroupId && lhsGroupId == rhsGroupId) { - throw new Error( - `The left-hand id [${lhsId}] is modified to traverse the group boundary, but the edge does not pass through two groups.` - ); - } - if (rhsGroup && lhsGroupId && rhsGroupId && lhsGroupId == rhsGroupId) { - throw new Error( - `The right-hand id [${rhsId}] is modified to traverse the group boundary, but the edge does not pass through two groups.` - ); + public getJunctions(): ArchitectureJunction[] { + return Object.values(this.nodes).filter(isArchitectureJunction); } - const edge = { + public getNodes(): ArchitectureNode[] { + return Object.values(this.nodes); + } + + public getNode(id: string): ArchitectureNode | null { + return this.nodes[id] ?? null; + } + + public addGroup({ id, icon, in: parent, title }: ArchitectureGroup): void { + if (this.registeredIds?.[id] !== undefined) { + throw new Error( + `The group id [${id}] is already in use by another ${this.registeredIds[id]}` + ); + } + if (parent !== undefined) { + if (id === parent) { + throw new Error(`The group [${id}] cannot be placed within itself`); + } + if (this.registeredIds?.[parent] === undefined) { + throw new Error( + `The group [${id}]'s parent does not exist. Please make sure the parent is created before this group` + ); + } + if (this.registeredIds?.[parent] === 'node') { + throw new Error(`The group [${id}]'s parent is not a group`); + } + } + + this.registeredIds[id] = 'group'; + + this.groups[id] = { + id, + icon, + title, + in: parent, + }; + } + public getGroups(): ArchitectureGroup[] { + return Object.values(this.groups); + } + public addEdge({ lhsId, - lhsDir, - lhsInto, - lhsGroup, rhsId, + lhsDir, rhsDir, + lhsInto, rhsInto, + lhsGroup, rhsGroup, title, - }; - - state.records.edges.push(edge); - if (state.records.nodes[lhsId] && state.records.nodes[rhsId]) { - state.records.nodes[lhsId].edges.push(state.records.edges[state.records.edges.length - 1]); - state.records.nodes[rhsId].edges.push(state.records.edges[state.records.edges.length - 1]); - } -}; - -const getEdges = (): ArchitectureEdge[] => state.records.edges; - -/** - * 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> - > = {}; - - // 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 - const adjList = Object.entries(state.records.nodes).reduce< - Record - >((prevOuter, [id, service]) => { - prevOuter[id] = service.edges.reduce((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); - if (pair) { - prevInner[pair] = edge.rhsId; - } - } else { - // source is RHS - const pair = getArchitectureDirectionPair(edge.rhsDir, edge.lhsDir); - if (pair) { - prevInner[pair] = edge.lhsId; - } - } - return prevInner; - }, {}); - return prevOuter; - }, {}); - - // 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 - ); - - // Perform BFS on the adjacency list - const BFS = (startingId: string): ArchitectureSpatialMap => { - const spatialMap = { [startingId]: [0, 0] }; - const queue = [startingId]; - while (queue.length > 0) { - const id = queue.shift(); - if (id) { - visited[id] = 1; - delete notVisited[id]; - const adj = adjList[id]; - const [posX, posY] = spatialMap[id]; - Object.entries(adj).forEach(([dir, rhsId]) => { - if (!visited[rhsId]) { - spatialMap[rhsId] = shiftPositionByArchitectureDirectionPair( - [posX, posY], - dir as ArchitectureDirectionPair - ); - queue.push(rhsId); - } - }); - } - } - return spatialMap; - }; - const spatialMaps = [BFS(firstId)]; - - // If our diagram is disconnected, keep adding additional spatial maps until all disconnected graphs have been found - while (Object.keys(notVisited).length > 0) { - spatialMaps.push(BFS(Object.keys(notVisited)[0])); + }: ArchitectureEdge): void { + if (!isArchitectureDirection(lhsDir)) { + throw new Error( + `Invalid direction given for left hand side of edge ${lhsId}--${rhsId}. Expected (L,R,T,B) got ${String(lhsDir)}` + ); } - state.records.dataStructures = { - adjList, - spatialMaps, - groupAlignments, + if (!isArchitectureDirection(rhsDir)) { + throw new Error( + `Invalid direction given for right hand side of edge ${lhsId}--${rhsId}. Expected (L,R,T,B) got ${String(rhsDir)}` + ); + } + + if (this.nodes[lhsId] === undefined && this.groups[lhsId] === undefined) { + throw new Error( + `The left-hand id [${lhsId}] does not yet exist. Please create the service/group before declaring an edge to it.` + ); + } + if (this.nodes[rhsId] === undefined && this.groups[lhsId] === undefined) { + throw new Error( + `The right-hand id [${rhsId}] does not yet exist. Please create the service/group before declaring an edge to it.` + ); + } + + const lhsGroupId = this.nodes[lhsId].in; + const rhsGroupId = this.nodes[rhsId].in; + if (lhsGroup && lhsGroupId && rhsGroupId && lhsGroupId == rhsGroupId) { + throw new Error( + `The left-hand id [${lhsId}] is modified to traverse the group boundary, but the edge does not pass through two groups.` + ); + } + if (rhsGroup && lhsGroupId && rhsGroupId && lhsGroupId == rhsGroupId) { + throw new Error( + `The right-hand id [${rhsId}] is modified to traverse the group boundary, but the edge does not pass through two groups.` + ); + } + + const edge = { + lhsId, + lhsDir, + lhsInto, + lhsGroup, + rhsId, + rhsDir, + rhsInto, + rhsGroup, + title, }; + + this.edges.push(edge); + if (this.nodes[lhsId] && this.nodes[rhsId]) { + this.nodes[lhsId].edges.push(this.edges[this.edges.length - 1]); + this.nodes[rhsId].edges.push(this.edges[this.edges.length - 1]); + } } - return state.records.dataStructures; -}; -const setElementForId = (id: string, element: D3Element) => { - state.records.elements[id] = element; -}; -const getElementById = (id: string) => state.records.elements[id]; + public getEdges(): ArchitectureEdge[] { + return this.edges; + } -const getConfig = (): Required => { - const config = cleanAndMerge({ - ...DEFAULT_ARCHITECTURE_CONFIG, - ...commonGetConfig().architecture, - }); - return config; -}; + /** + * Returns the current diagram's adjacency list, spatial map, & group alignments. + * If they have not been created, run the algorithms to generate them. + * @returns + */ + public getDataStructures() { + if (this.dataStructures === undefined) { + // Tracks how groups are aligned with one another. Generated while creating the adj list + const groupAlignments: Record< + string, + Record> + > = {}; -export const db: ArchitectureDB = { - clear, - setDiagramTitle, - getDiagramTitle, - setAccTitle, - getAccTitle, - setAccDescription, - getAccDescription, - getConfig, + // 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 + const adjList = Object.entries(this.nodes).reduce< + Record + >((prevOuter, [id, service]) => { + prevOuter[id] = service.edges.reduce((prevInner, edge) => { + // track the direction groups connect to one another + const lhsGroupId = this.getNode(edge.lhsId)?.in; + const rhsGroupId = this.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; + } + } - addService, - getServices, - addJunction, - getJunctions, - getNodes, - getNode, - addGroup, - getGroups, - addEdge, - getEdges, - setElementForId, - getElementById, - getDataStructures, -}; + if (edge.lhsId === id) { + // source is LHS + const pair = getArchitectureDirectionPair(edge.lhsDir, edge.rhsDir); + if (pair) { + prevInner[pair] = edge.rhsId; + } + } else { + // source is RHS + const pair = getArchitectureDirectionPair(edge.rhsDir, edge.lhsDir); + if (pair) { + prevInner[pair] = edge.lhsId; + } + } + return prevInner; + }, {}); + return prevOuter; + }, {}); + + // 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 + ); + + // Perform BFS on the adjacency list + const BFS = (startingId: string): ArchitectureSpatialMap => { + const spatialMap = { [startingId]: [0, 0] }; + const queue = [startingId]; + while (queue.length > 0) { + const id = queue.shift(); + if (id) { + visited[id] = 1; + delete notVisited[id]; + const adj = adjList[id]; + const [posX, posY] = spatialMap[id]; + Object.entries(adj).forEach(([dir, rhsId]) => { + if (!visited[rhsId]) { + spatialMap[rhsId] = shiftPositionByArchitectureDirectionPair( + [posX, posY], + dir as ArchitectureDirectionPair + ); + queue.push(rhsId); + } + }); + } + } + return spatialMap; + }; + const spatialMaps = [BFS(firstId)]; + + // If our diagram is disconnected, keep adding additional spatial maps until all disconnected graphs have been found + while (Object.keys(notVisited).length > 0) { + spatialMaps.push(BFS(Object.keys(notVisited)[0])); + } + this.dataStructures = { + adjList, + spatialMaps, + groupAlignments, + }; + } + return this.dataStructures; + } + + public setElementForId(id: string, element: D3Element): void { + this.elements[id] = element; + } + + public getElementById(id: string): D3Element { + return this.elements[id]; + } + + public getConfig(): Required { + return cleanAndMerge({ + ...DEFAULT_ARCHITECTURE_CONFIG, + ...commonGetConfig().architecture, + }); + } + + public getConfigField( + field: T + ): Required[T] { + return this.getConfig()[field]; + } + + public setAccTitle = setAccTitle; + public getAccTitle = getAccTitle; + public setDiagramTitle = setDiagramTitle; + public getDiagramTitle = getDiagramTitle; + public getAccDescription = getAccDescription; + public setAccDescription = setAccDescription; +} /** * Typed wrapper for resolving an architecture diagram's config fields. Returns the default value if undefined * @param field - the config field to access * @returns */ -export function getConfigField( - field: T -): Required[T] { - return getConfig()[field]; -} +// export function getConfigField( +// field: T +// ): Required[T] { +// return db.getConfig()[field]; +// } diff --git a/packages/mermaid/src/diagrams/architecture/architectureDiagram.ts b/packages/mermaid/src/diagrams/architecture/architectureDiagram.ts index 82dacd3e1..1d390a3ab 100644 --- a/packages/mermaid/src/diagrams/architecture/architectureDiagram.ts +++ b/packages/mermaid/src/diagrams/architecture/architectureDiagram.ts @@ -1,12 +1,14 @@ import type { DiagramDefinition } from '../../diagram-api/types.js'; import { parser } from './architectureParser.js'; -import { db } from './architectureDb.js'; +import { ArchitectureDB } from './architectureDb.js'; import styles from './architectureStyles.js'; import { renderer } from './architectureRenderer.js'; export const diagram: DiagramDefinition = { parser, - db, + get db() { + return new ArchitectureDB(); + }, renderer, styles, }; diff --git a/packages/mermaid/src/diagrams/architecture/architectureParser.ts b/packages/mermaid/src/diagrams/architecture/architectureParser.ts index a7159d907..f3d7771be 100644 --- a/packages/mermaid/src/diagrams/architecture/architectureParser.ts +++ b/packages/mermaid/src/diagrams/architecture/architectureParser.ts @@ -1,24 +1,33 @@ import type { Architecture } from '@mermaid-js/parser'; import { parse } from '@mermaid-js/parser'; -import { log } from '../../logger.js'; import type { ParserDefinition } from '../../diagram-api/types.js'; +import { log } from '../../logger.js'; import { populateCommonDb } from '../common/populateCommonDb.js'; -import type { ArchitectureDB } from './architectureTypes.js'; -import { db } from './architectureDb.js'; +import { ArchitectureDB } from './architectureDb.js'; const populateDb = (ast: Architecture, db: ArchitectureDB) => { populateCommonDb(ast, db); - ast.groups.map(db.addGroup); + ast.groups.map((group) => db.addGroup(group)); ast.services.map((service) => db.addService({ ...service, type: 'service' })); ast.junctions.map((service) => db.addJunction({ ...service, type: 'junction' })); - // @ts-ignore TODO our parser guarantees the type is L/R/T/B and not string. How to change to union type? - ast.edges.map(db.addEdge); + // @ts-ignore ToDo our parser guarantees the type is L/R/T/B and not string. How to change to union type? + ast.edges.map((edge) => db.addEdge(edge)); }; export const parser: ParserDefinition = { + parser: { + // @ts-expect-error - ArchitectureDB is not assignable to DiagramDB + yy: undefined, + }, parse: async (input: string): Promise => { const ast: Architecture = await parse('architecture', input); log.debug(ast); + const db = parser.parser?.yy; + if (!(db instanceof ArchitectureDB)) { + throw new Error( + 'parser.parser?.yy was not a ArchitectureDB. This is due to a bug within Mermaid, please report this issue at https://github.com/mermaid-js/mermaid/issues.' + ); + } populateDb(ast, db); }, }; diff --git a/packages/mermaid/src/diagrams/architecture/architectureRenderer.ts b/packages/mermaid/src/diagrams/architecture/architectureRenderer.ts index 1505b1950..ad435a29f 100644 --- a/packages/mermaid/src/diagrams/architecture/architectureRenderer.ts +++ b/packages/mermaid/src/diagrams/architecture/architectureRenderer.ts @@ -1,4 +1,3 @@ -import { registerIconPacks } from '../../rendering-util/icons.js'; import type { Position } from 'cytoscape'; import cytoscape from 'cytoscape'; import type { FcoseLayoutOptions } from 'cytoscape-fcose'; @@ -7,9 +6,10 @@ import { select } from 'd3'; import type { DrawDefinition, SVG } from '../../diagram-api/types.js'; import type { Diagram } from '../../Diagram.js'; import { log } from '../../logger.js'; +import { registerIconPacks } from '../../rendering-util/icons.js'; import { selectSvgElement } from '../../rendering-util/selectSvgElement.js'; import { setupGraphViewbox } from '../../setupGraphViewbox.js'; -import { getConfigField } from './architectureDb.js'; +import type { ArchitectureDB } from './architectureDb.js'; import { architectureIcons } from './architectureIcons.js'; import type { ArchitectureAlignment, @@ -22,7 +22,6 @@ import type { NodeSingularData, } from './architectureTypes.js'; import { - type ArchitectureDB, type ArchitectureDirection, type ArchitectureEdge, type ArchitectureGroup, @@ -44,7 +43,7 @@ registerIconPacks([ ]); cytoscape.use(fcose); -function addServices(services: ArchitectureService[], cy: cytoscape.Core) { +function addServices(services: ArchitectureService[], cy: cytoscape.Core, db: ArchitectureDB) { services.forEach((service) => { cy.add({ group: 'nodes', @@ -54,15 +53,15 @@ function addServices(services: ArchitectureService[], cy: cytoscape.Core) { icon: service.icon, label: service.title, parent: service.in, - width: getConfigField('iconSize'), - height: getConfigField('iconSize'), + width: db.getConfigField('iconSize'), + height: db.getConfigField('iconSize'), } as NodeSingularData, classes: 'node-service', }); }); } -function addJunctions(junctions: ArchitectureJunction[], cy: cytoscape.Core) { +function addJunctions(junctions: ArchitectureJunction[], cy: cytoscape.Core, db: ArchitectureDB) { junctions.forEach((junction) => { cy.add({ group: 'nodes', @@ -70,8 +69,8 @@ function addJunctions(junctions: ArchitectureJunction[], cy: cytoscape.Core) { type: 'junction', id: junction.id, parent: junction.in, - width: getConfigField('iconSize'), - height: getConfigField('iconSize'), + width: db.getConfigField('iconSize'), + height: db.getConfigField('iconSize'), } as NodeSingularData, classes: 'node-junction', }); @@ -192,7 +191,7 @@ function getAlignments( 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 + // If either of the groups are in the default space (not in a group), use the same behaviour as above prev[dir] ??= []; prev[dir] = [...prev[dir], ...aNodeIds, ...bNodeIds]; } else { @@ -257,7 +256,8 @@ function getAlignments( } function getRelativeConstraints( - spatialMaps: ArchitectureSpatialMap[] + spatialMaps: ArchitectureSpatialMap[], + db: ArchitectureDB ): fcose.FcoseRelativePlacementConstraint[] { const relativeConstraints: fcose.FcoseRelativePlacementConstraint[] = []; const posToStr = (pos: number[]) => `${pos[0]},${pos[1]}`; @@ -296,7 +296,7 @@ function getRelativeConstraints( [ArchitectureDirectionName[ getOppositeArchitectureDirection(dir as ArchitectureDirection) ]]: currId, - gap: 1.5 * getConfigField('iconSize'), + gap: 1.5 * db.getConfigField('iconSize'), }); } }); @@ -353,7 +353,7 @@ function layoutArchitecture( style: { 'text-valign': 'bottom', 'text-halign': 'center', - 'font-size': `${getConfigField('fontSize')}px`, + 'font-size': `${db.getConfigField('fontSize')}px`, }, }, { @@ -375,7 +375,7 @@ function layoutArchitecture( selector: '.node-group', style: { // @ts-ignore Incorrect library types - padding: `${getConfigField('padding')}px`, + padding: `${db.getConfigField('padding')}px`, }, }, ], @@ -393,14 +393,14 @@ function layoutArchitecture( renderEl.remove(); addGroups(groups, cy); - addServices(services, cy); - addJunctions(junctions, cy); + addServices(services, cy, db); + addJunctions(junctions, cy, db); addEdges(edges, cy); // Use the spatial map to create alignment arrays for fcose 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); + const relativePlacementConstraint = getRelativeConstraints(spatialMaps, db); const layout = cy.layout({ name: 'fcose', @@ -415,7 +415,9 @@ function layoutArchitecture( const { parent: parentA } = nodeData(nodeA); const { parent: parentB } = nodeData(nodeB); const elasticity = - parentA === parentB ? 1.5 * getConfigField('iconSize') : 0.5 * getConfigField('iconSize'); + parentA === parentB + ? 1.5 * db.getConfigField('iconSize') + : 0.5 * db.getConfigField('iconSize'); return elasticity; }, edgeElasticity(edge: EdgeSingular) { @@ -535,11 +537,11 @@ export const draw: DrawDefinition = async (text, id, _version, diagObj: Diagram) const cy = await layoutArchitecture(services, junctions, groups, edges, db, ds); - await drawEdges(edgesElem, cy); - await drawGroups(groupElem, cy); + await drawEdges(edgesElem, cy, db); + await drawGroups(groupElem, cy, db); positionNodes(db, cy); - setupGraphViewbox(undefined, svg, getConfigField('padding'), getConfigField('useMaxWidth')); + setupGraphViewbox(undefined, svg, db.getConfigField('padding'), db.getConfigField('useMaxWidth')); }; export const renderer = { draw }; diff --git a/packages/mermaid/src/diagrams/architecture/svgDraw.ts b/packages/mermaid/src/diagrams/architecture/svgDraw.ts index b10a451fe..f384defd8 100644 --- a/packages/mermaid/src/diagrams/architecture/svgDraw.ts +++ b/packages/mermaid/src/diagrams/architecture/svgDraw.ts @@ -1,9 +1,9 @@ -import { getIconSVG } from '../../rendering-util/icons.js'; import type cytoscape from 'cytoscape'; import { getConfig } from '../../diagram-api/diagramAPI.js'; import { createText } from '../../rendering-util/createText.js'; +import { getIconSVG } from '../../rendering-util/icons.js'; import type { D3Element } from '../../types.js'; -import { db, getConfigField } from './architectureDb.js'; +import type { ArchitectureDB } from './architectureDb.js'; import { architectureIcons } from './architectureIcons.js'; import { ArchitectureDirectionArrow, @@ -16,14 +16,17 @@ import { isArchitectureDirectionY, isArchitecturePairXY, nodeData, - type ArchitectureDB, type ArchitectureJunction, type ArchitectureService, } from './architectureTypes.js'; -export const drawEdges = async function (edgesEl: D3Element, cy: cytoscape.Core) { - const padding = getConfigField('padding'); - const iconSize = getConfigField('iconSize'); +export const drawEdges = async function ( + edgesEl: D3Element, + cy: cytoscape.Core, + db: ArchitectureDB +) { + const padding = db.getConfigField('padding'); + const iconSize = db.getConfigField('iconSize'); const halfIconSize = iconSize / 2; const arrowSize = iconSize / 6; const halfArrowSize = arrowSize / 2; @@ -183,13 +186,17 @@ export const drawEdges = async function (edgesEl: D3Element, cy: cytoscape.Core) ); }; -export const drawGroups = async function (groupsEl: D3Element, cy: cytoscape.Core) { - const padding = getConfigField('padding'); +export const drawGroups = async function ( + groupsEl: D3Element, + cy: cytoscape.Core, + db: ArchitectureDB +) { + const padding = db.getConfigField('padding'); const groupIconSize = padding * 0.75; - const fontSize = getConfigField('fontSize'); + const fontSize = db.getConfigField('fontSize'); - const iconSize = getConfigField('iconSize'); + const iconSize = db.getConfigField('iconSize'); const halfIconSize = iconSize / 2; await Promise.all( @@ -266,7 +273,7 @@ export const drawServices = async function ( ): Promise { for (const service of services) { const serviceElem = elem.append('g'); - const iconSize = getConfigField('iconSize'); + const iconSize = db.getConfigField('iconSize'); if (service.title) { const textElem = serviceElem.append('g'); @@ -350,7 +357,7 @@ export const drawJunctions = function ( ) { junctions.forEach((junction) => { const junctionElem = elem.append('g'); - const iconSize = getConfigField('iconSize'); + const iconSize = db.getConfigField('iconSize'); const bkgElem = junctionElem.append('g'); bkgElem