6774: update architecture diagram to reflect new class-based DB structure

on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
This commit is contained in:
omkarht
2025-07-29 15:21:34 +05:30
parent 0cc0b63e52
commit 1a4b8662cf
6 changed files with 364 additions and 349 deletions

View File

@@ -1,21 +1,12 @@
import { it, describe, expect } from 'vitest'; import { it, describe, expect } from 'vitest';
import { db } from './architectureDb.js';
import { parser } from './architectureParser.js'; import { parser } from './architectureParser.js';
import { ArchitectureDB } from './architectureDb.js';
const {
clear,
getDiagramTitle,
getAccTitle,
getAccDescription,
getServices,
getGroups,
getEdges,
getJunctions,
} = db;
describe('architecture diagrams', () => { describe('architecture diagrams', () => {
let db: ArchitectureDB;
beforeEach(() => { 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', () => { describe('architecture diagram definitions', () => {
@@ -36,7 +27,7 @@ describe('architecture diagrams', () => {
it('should handle title on the first line', async () => { it('should handle title on the first line', async () => {
const str = `architecture-beta title Simple Architecture Diagram`; const str = `architecture-beta title Simple Architecture Diagram`;
await expect(parser.parse(str)).resolves.not.toThrow(); 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 () => { it('should handle title on another line', async () => {
@@ -44,7 +35,7 @@ describe('architecture diagrams', () => {
title Simple Architecture Diagram title Simple Architecture Diagram
`; `;
await expect(parser.parse(str)).resolves.not.toThrow(); 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 () => { it('should handle accessibility title and description', async () => {
@@ -53,8 +44,8 @@ describe('architecture diagrams', () => {
accDescr: Accessibility Description accDescr: Accessibility Description
`; `;
await expect(parser.parse(str)).resolves.not.toThrow(); await expect(parser.parse(str)).resolves.not.toThrow();
expect(getAccTitle()).toBe('Accessibility Title'); expect(db.getAccTitle()).toBe('Accessibility Title');
expect(getAccDescription()).toBe('Accessibility Description'); expect(db.getAccDescription()).toBe('Accessibility Description');
}); });
it('should handle multiline accessibility description', async () => { it('should handle multiline accessibility description', async () => {
@@ -64,7 +55,7 @@ describe('architecture diagrams', () => {
} }
`; `;
await expect(parser.parse(str)).resolves.not.toThrow(); await expect(parser.parse(str)).resolves.not.toThrow();
expect(getAccDescription()).toBe('Accessibility Description'); expect(db.getAccDescription()).toBe('Accessibility Description');
}); });
}); });
}); });

View File

@@ -1,8 +1,9 @@
import { getConfig as commonGetConfig } from '../../config.js';
import type { ArchitectureDiagramConfig } from '../../config.type.js'; import type { ArchitectureDiagramConfig } from '../../config.type.js';
import DEFAULT_CONFIG from '../../defaultConfig.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 type { D3Element } from '../../types.js';
import { ImperativeState } from '../../utils/imperativeState.js'; import { cleanAndMerge } from '../../utils.js';
import { import {
clear as commonClear, clear as commonClear,
getAccDescription, getAccDescription,
@@ -14,7 +15,6 @@ import {
} from '../common/commonDb.js'; } from '../common/commonDb.js';
import type { import type {
ArchitectureAlignment, ArchitectureAlignment,
ArchitectureDB,
ArchitectureDirectionPair, ArchitectureDirectionPair,
ArchitectureDirectionPairMap, ArchitectureDirectionPairMap,
ArchitectureEdge, ArchitectureEdge,
@@ -33,330 +33,334 @@ import {
isArchitectureService, isArchitectureService,
shiftPositionByArchitectureDirectionPair, shiftPositionByArchitectureDirectionPair,
} from './architectureTypes.js'; } from './architectureTypes.js';
import { cleanAndMerge } from '../../utils.js';
const DEFAULT_ARCHITECTURE_CONFIG: Required<ArchitectureDiagramConfig> = const DEFAULT_ARCHITECTURE_CONFIG: Required<ArchitectureDiagramConfig> =
DEFAULT_CONFIG.architecture; DEFAULT_CONFIG.architecture;
export class ArchitectureDB implements DiagramDB {
private nodes: Record<string, ArchitectureNode> = {};
private groups: Record<string, ArchitectureGroup> = {};
private edges: ArchitectureEdge[] = [];
private registeredIds: Record<string, 'node' | 'group'> = {};
// private config: Required<ArchitectureDiagramConfig> = DEFAULT_ARCHITECTURE_CONFIG;
private dataStructures?: ArchitectureState['dataStructures'];
private elements: Record<string, D3Element> = {};
const state = new ImperativeState<ArchitectureState>(() => ({ constructor() {
nodes: {}, this.clear();
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<ArchitectureService, 'edges'>) {
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`);
}
} }
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, id,
type: 'service',
icon, icon,
in: parent,
title,
iconText, iconText,
title, }: Omit<ArchitectureService, 'edges'>): void {
edges: [], if (this.registeredIds[id] !== undefined) {
in: parent,
};
};
const getServices = (): ArchitectureService[] =>
Object.values(state.records.nodes).filter<ArchitectureService>(isArchitectureService);
const addJunction = function ({ id, in: parent }: Omit<ArchitectureJunction, 'edges'>) {
state.records.registeredIds[id] = 'node';
state.records.nodes[id] = {
id,
type: 'junction',
edges: [],
in: parent,
};
};
const getJunctions = (): ArchitectureJunction[] =>
Object.values(state.records.nodes).filter<ArchitectureJunction>(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) {
throw new Error( 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') { if (parent !== undefined) {
throw new Error(`The group [${id}]'s parent is not a group`); 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'; public getServices(): ArchitectureService[] {
return Object.values(this.nodes).filter(isArchitectureService);
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<string>) {
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}`
);
} }
if (state.records.nodes[lhsId] === undefined && state.records.groups[lhsId] === undefined) { public addJunction({ id, in: parent }: Omit<ArchitectureJunction, 'edges'>): void {
throw new Error( this.registeredIds[id] = 'node';
`The left-hand id [${lhsId}] does not yet exist. Please create the service/group before declaring an edge to it.`
); this.nodes[id] = {
} id,
if (state.records.nodes[rhsId] === undefined && state.records.groups[lhsId] === undefined) { type: 'junction',
throw new Error( edges: [],
`The right-hand id [${rhsId}] does not yet exist. Please create the service/group before declaring an edge to it.` in: parent,
); };
} }
const lhsGroupId = state.records.nodes[lhsId].in; public getJunctions(): ArchitectureJunction[] {
const rhsGroupId = state.records.nodes[rhsId].in; return Object.values(this.nodes).filter(isArchitectureJunction);
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 = { 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, lhsId,
lhsDir,
lhsInto,
lhsGroup,
rhsId, rhsId,
lhsDir,
rhsDir, rhsDir,
lhsInto,
rhsInto, rhsInto,
lhsGroup,
rhsGroup, rhsGroup,
title, title,
}; }: ArchitectureEdge): void {
if (!isArchitectureDirection(lhsDir)) {
state.records.edges.push(edge); throw new Error(
if (state.records.nodes[lhsId] && state.records.nodes[rhsId]) { `Invalid direction given for left hand side of edge ${lhsId}--${rhsId}. Expected (L,R,T,B) got ${String(lhsDir)}`
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<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
const adjList = Object.entries(state.records.nodes).reduce<
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);
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<string, number>
);
// 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]));
} }
state.records.dataStructures = { if (!isArchitectureDirection(rhsDir)) {
adjList, throw new Error(
spatialMaps, `Invalid direction given for right hand side of edge ${lhsId}--${rhsId}. Expected (L,R,T,B) got ${String(rhsDir)}`
groupAlignments, );
}
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) => { public getEdges(): ArchitectureEdge[] {
state.records.elements[id] = element; return this.edges;
}; }
const getElementById = (id: string) => state.records.elements[id];
const getConfig = (): Required<ArchitectureDiagramConfig> => { /**
const config = cleanAndMerge({ * Returns the current diagram's adjacency list, spatial map, & group alignments.
...DEFAULT_ARCHITECTURE_CONFIG, * If they have not been created, run the algorithms to generate them.
...commonGetConfig().architecture, * @returns
}); */
return config; 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<string, Exclude<ArchitectureAlignment, 'bend'>>
> = {};
export const db: ArchitectureDB = { // Create an adjacency list of the diagram to perform BFS on
clear, // Outer reduce applied on all services
setDiagramTitle, // Inner reduce applied on the edges for a service
getDiagramTitle, const adjList = Object.entries(this.nodes).reduce<
setAccTitle, Record<string, ArchitectureDirectionPairMap>
getAccTitle, >((prevOuter, [id, service]) => {
setAccDescription, prevOuter[id] = service.edges.reduce<ArchitectureDirectionPairMap>((prevInner, edge) => {
getAccDescription, // track the direction groups connect to one another
getConfig, 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, if (edge.lhsId === id) {
getServices, // source is LHS
addJunction, const pair = getArchitectureDirectionPair(edge.lhsDir, edge.rhsDir);
getJunctions, if (pair) {
getNodes, prevInner[pair] = edge.rhsId;
getNode, }
addGroup, } else {
getGroups, // source is RHS
addEdge, const pair = getArchitectureDirectionPair(edge.rhsDir, edge.lhsDir);
getEdges, if (pair) {
setElementForId, prevInner[pair] = edge.lhsId;
getElementById, }
getDataStructures, }
}; 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<string, number>
);
// 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<ArchitectureDiagramConfig> {
return cleanAndMerge({
...DEFAULT_ARCHITECTURE_CONFIG,
...commonGetConfig().architecture,
});
}
public getConfigField<T extends keyof ArchitectureDiagramConfig>(
field: T
): Required<ArchitectureDiagramConfig>[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 * Typed wrapper for resolving an architecture diagram's config fields. Returns the default value if undefined
* @param field - the config field to access * @param field - the config field to access
* @returns * @returns
*/ */
export function getConfigField<T extends keyof ArchitectureDiagramConfig>( // export function getConfigField<T extends key of ArchitectureDiagramConfig>(
field: T // field: T
): Required<ArchitectureDiagramConfig>[T] { // ): Required<ArchitectureDiagramConfig>[T] {
return getConfig()[field]; // return db.getConfig()[field];
} // }

View File

@@ -1,12 +1,14 @@
import type { DiagramDefinition } from '../../diagram-api/types.js'; import type { DiagramDefinition } from '../../diagram-api/types.js';
import { parser } from './architectureParser.js'; import { parser } from './architectureParser.js';
import { db } from './architectureDb.js'; import { ArchitectureDB } from './architectureDb.js';
import styles from './architectureStyles.js'; import styles from './architectureStyles.js';
import { renderer } from './architectureRenderer.js'; import { renderer } from './architectureRenderer.js';
export const diagram: DiagramDefinition = { export const diagram: DiagramDefinition = {
parser, parser,
db, get db() {
return new ArchitectureDB();
},
renderer, renderer,
styles, styles,
}; };

View File

@@ -1,24 +1,33 @@
import type { Architecture } from '@mermaid-js/parser'; import type { Architecture } from '@mermaid-js/parser';
import { parse } from '@mermaid-js/parser'; import { parse } from '@mermaid-js/parser';
import { log } from '../../logger.js';
import type { ParserDefinition } from '../../diagram-api/types.js'; import type { ParserDefinition } from '../../diagram-api/types.js';
import { log } from '../../logger.js';
import { populateCommonDb } from '../common/populateCommonDb.js'; import { populateCommonDb } from '../common/populateCommonDb.js';
import type { ArchitectureDB } from './architectureTypes.js'; import { ArchitectureDB } from './architectureDb.js';
import { db } from './architectureDb.js';
const populateDb = (ast: Architecture, db: ArchitectureDB) => { const populateDb = (ast: Architecture, db: ArchitectureDB) => {
populateCommonDb(ast, db); 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.services.map((service) => db.addService({ ...service, type: 'service' }));
ast.junctions.map((service) => db.addJunction({ ...service, type: 'junction' })); 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? // @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); ast.edges.map((edge) => db.addEdge(edge));
}; };
export const parser: ParserDefinition = { export const parser: ParserDefinition = {
parser: {
// @ts-expect-error - ArchitectureDB is not assignable to DiagramDB
yy: undefined,
},
parse: async (input: string): Promise<void> => { parse: async (input: string): Promise<void> => {
const ast: Architecture = await parse('architecture', input); const ast: Architecture = await parse('architecture', input);
log.debug(ast); 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); populateDb(ast, db);
}, },
}; };

View File

@@ -1,4 +1,3 @@
import { registerIconPacks } from '../../rendering-util/icons.js';
import type { Position } from 'cytoscape'; import type { Position } from 'cytoscape';
import cytoscape from 'cytoscape'; import cytoscape from 'cytoscape';
import type { FcoseLayoutOptions } from 'cytoscape-fcose'; 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 { DrawDefinition, SVG } from '../../diagram-api/types.js';
import type { Diagram } from '../../Diagram.js'; import type { Diagram } from '../../Diagram.js';
import { log } from '../../logger.js'; import { log } from '../../logger.js';
import { registerIconPacks } from '../../rendering-util/icons.js';
import { selectSvgElement } from '../../rendering-util/selectSvgElement.js'; import { selectSvgElement } from '../../rendering-util/selectSvgElement.js';
import { setupGraphViewbox } from '../../setupGraphViewbox.js'; import { setupGraphViewbox } from '../../setupGraphViewbox.js';
import { getConfigField } from './architectureDb.js'; import type { ArchitectureDB } from './architectureDb.js';
import { architectureIcons } from './architectureIcons.js'; import { architectureIcons } from './architectureIcons.js';
import type { import type {
ArchitectureAlignment, ArchitectureAlignment,
@@ -22,7 +22,6 @@ import type {
NodeSingularData, NodeSingularData,
} from './architectureTypes.js'; } from './architectureTypes.js';
import { import {
type ArchitectureDB,
type ArchitectureDirection, type ArchitectureDirection,
type ArchitectureEdge, type ArchitectureEdge,
type ArchitectureGroup, type ArchitectureGroup,
@@ -44,7 +43,7 @@ registerIconPacks([
]); ]);
cytoscape.use(fcose); cytoscape.use(fcose);
function addServices(services: ArchitectureService[], cy: cytoscape.Core) { function addServices(services: ArchitectureService[], cy: cytoscape.Core, db: ArchitectureDB) {
services.forEach((service) => { services.forEach((service) => {
cy.add({ cy.add({
group: 'nodes', group: 'nodes',
@@ -54,15 +53,15 @@ function addServices(services: ArchitectureService[], cy: cytoscape.Core) {
icon: service.icon, icon: service.icon,
label: service.title, label: service.title,
parent: service.in, parent: service.in,
width: getConfigField('iconSize'), width: db.getConfigField('iconSize'),
height: getConfigField('iconSize'), height: db.getConfigField('iconSize'),
} as NodeSingularData, } as NodeSingularData,
classes: 'node-service', classes: 'node-service',
}); });
}); });
} }
function addJunctions(junctions: ArchitectureJunction[], cy: cytoscape.Core) { function addJunctions(junctions: ArchitectureJunction[], cy: cytoscape.Core, db: ArchitectureDB) {
junctions.forEach((junction) => { junctions.forEach((junction) => {
cy.add({ cy.add({
group: 'nodes', group: 'nodes',
@@ -70,8 +69,8 @@ function addJunctions(junctions: ArchitectureJunction[], cy: cytoscape.Core) {
type: 'junction', type: 'junction',
id: junction.id, id: junction.id,
parent: junction.in, parent: junction.in,
width: getConfigField('iconSize'), width: db.getConfigField('iconSize'),
height: getConfigField('iconSize'), height: db.getConfigField('iconSize'),
} as NodeSingularData, } as NodeSingularData,
classes: 'node-junction', classes: 'node-junction',
}); });
@@ -192,7 +191,7 @@ function getAlignments(
prev[dir] ??= []; prev[dir] ??= [];
prev[dir] = [...prev[dir], ...aNodeIds, ...bNodeIds]; // add the node ids of both groups to the axis array in prev 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') { } 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] = [...prev[dir], ...aNodeIds, ...bNodeIds]; prev[dir] = [...prev[dir], ...aNodeIds, ...bNodeIds];
} else { } else {
@@ -257,7 +256,8 @@ function getAlignments(
} }
function getRelativeConstraints( function getRelativeConstraints(
spatialMaps: ArchitectureSpatialMap[] spatialMaps: ArchitectureSpatialMap[],
db: ArchitectureDB
): fcose.FcoseRelativePlacementConstraint[] { ): fcose.FcoseRelativePlacementConstraint[] {
const relativeConstraints: fcose.FcoseRelativePlacementConstraint[] = []; const relativeConstraints: fcose.FcoseRelativePlacementConstraint[] = [];
const posToStr = (pos: number[]) => `${pos[0]},${pos[1]}`; const posToStr = (pos: number[]) => `${pos[0]},${pos[1]}`;
@@ -296,7 +296,7 @@ function getRelativeConstraints(
[ArchitectureDirectionName[ [ArchitectureDirectionName[
getOppositeArchitectureDirection(dir as ArchitectureDirection) getOppositeArchitectureDirection(dir as ArchitectureDirection)
]]: currId, ]]: currId,
gap: 1.5 * getConfigField('iconSize'), gap: 1.5 * db.getConfigField('iconSize'),
}); });
} }
}); });
@@ -353,7 +353,7 @@ function layoutArchitecture(
style: { style: {
'text-valign': 'bottom', 'text-valign': 'bottom',
'text-halign': 'center', 'text-halign': 'center',
'font-size': `${getConfigField('fontSize')}px`, 'font-size': `${db.getConfigField('fontSize')}px`,
}, },
}, },
{ {
@@ -375,7 +375,7 @@ function layoutArchitecture(
selector: '.node-group', selector: '.node-group',
style: { style: {
// @ts-ignore Incorrect library types // @ts-ignore Incorrect library types
padding: `${getConfigField('padding')}px`, padding: `${db.getConfigField('padding')}px`,
}, },
}, },
], ],
@@ -393,14 +393,14 @@ function layoutArchitecture(
renderEl.remove(); renderEl.remove();
addGroups(groups, cy); addGroups(groups, cy);
addServices(services, cy); addServices(services, cy, db);
addJunctions(junctions, cy); addJunctions(junctions, cy, db);
addEdges(edges, cy); addEdges(edges, cy);
// Use the spatial map to create alignment arrays for fcose // Use the spatial map to create alignment arrays for fcose
const alignmentConstraint = getAlignments(db, spatialMaps, groupAlignments); 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 // 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({ const layout = cy.layout({
name: 'fcose', name: 'fcose',
@@ -415,7 +415,9 @@ function layoutArchitecture(
const { parent: parentA } = nodeData(nodeA); const { parent: parentA } = nodeData(nodeA);
const { parent: parentB } = nodeData(nodeB); const { parent: parentB } = nodeData(nodeB);
const elasticity = 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; return elasticity;
}, },
edgeElasticity(edge: EdgeSingular) { 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); const cy = await layoutArchitecture(services, junctions, groups, edges, db, ds);
await drawEdges(edgesElem, cy); await drawEdges(edgesElem, cy, db);
await drawGroups(groupElem, cy); await drawGroups(groupElem, cy, db);
positionNodes(db, cy); positionNodes(db, cy);
setupGraphViewbox(undefined, svg, getConfigField('padding'), getConfigField('useMaxWidth')); setupGraphViewbox(undefined, svg, db.getConfigField('padding'), db.getConfigField('useMaxWidth'));
}; };
export const renderer = { draw }; export const renderer = { draw };

View File

@@ -1,9 +1,9 @@
import { getIconSVG } from '../../rendering-util/icons.js';
import type cytoscape from 'cytoscape'; import type cytoscape from 'cytoscape';
import { getConfig } from '../../diagram-api/diagramAPI.js'; import { getConfig } from '../../diagram-api/diagramAPI.js';
import { createText } from '../../rendering-util/createText.js'; import { createText } from '../../rendering-util/createText.js';
import { getIconSVG } from '../../rendering-util/icons.js';
import type { D3Element } from '../../types.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 { architectureIcons } from './architectureIcons.js';
import { import {
ArchitectureDirectionArrow, ArchitectureDirectionArrow,
@@ -16,14 +16,17 @@ import {
isArchitectureDirectionY, isArchitectureDirectionY,
isArchitecturePairXY, isArchitecturePairXY,
nodeData, nodeData,
type ArchitectureDB,
type ArchitectureJunction, type ArchitectureJunction,
type ArchitectureService, type ArchitectureService,
} from './architectureTypes.js'; } from './architectureTypes.js';
export const drawEdges = async function (edgesEl: D3Element, cy: cytoscape.Core) { export const drawEdges = async function (
const padding = getConfigField('padding'); edgesEl: D3Element,
const iconSize = getConfigField('iconSize'); cy: cytoscape.Core,
db: ArchitectureDB
) {
const padding = db.getConfigField('padding');
const iconSize = db.getConfigField('iconSize');
const halfIconSize = iconSize / 2; const halfIconSize = iconSize / 2;
const arrowSize = iconSize / 6; const arrowSize = iconSize / 6;
const halfArrowSize = arrowSize / 2; 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) { export const drawGroups = async function (
const padding = getConfigField('padding'); groupsEl: D3Element,
cy: cytoscape.Core,
db: ArchitectureDB
) {
const padding = db.getConfigField('padding');
const groupIconSize = padding * 0.75; 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; const halfIconSize = iconSize / 2;
await Promise.all( await Promise.all(
@@ -266,7 +273,7 @@ export const drawServices = async function (
): Promise<number> { ): Promise<number> {
for (const service of services) { for (const service of services) {
const serviceElem = elem.append('g'); const serviceElem = elem.append('g');
const iconSize = getConfigField('iconSize'); const iconSize = db.getConfigField('iconSize');
if (service.title) { if (service.title) {
const textElem = serviceElem.append('g'); const textElem = serviceElem.append('g');
@@ -350,7 +357,7 @@ export const drawJunctions = function (
) { ) {
junctions.forEach((junction) => { junctions.forEach((junction) => {
const junctionElem = elem.append('g'); const junctionElem = elem.append('g');
const iconSize = getConfigField('iconSize'); const iconSize = db.getConfigField('iconSize');
const bkgElem = junctionElem.append('g'); const bkgElem = junctionElem.append('g');
bkgElem bkgElem