mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-08-15 06:19:24 +02:00
Compare commits
18 Commits
e12036ed6c
...
ipsep-cola
Author | SHA1 | Date | |
---|---|---|---|
![]() |
f06f1bacd3 | ||
![]() |
d93d9a521d | ||
![]() |
c99bce6bab | ||
![]() |
4ab98c2ec7 | ||
![]() |
aeb51e56e2 | ||
![]() |
ddcd8a5e73 | ||
![]() |
e464d080ef | ||
![]() |
1a9b94ca2d | ||
![]() |
e27a9da61d | ||
![]() |
03cf10003f | ||
![]() |
8e31fdb611 | ||
![]() |
5dd748148f | ||
![]() |
895f9d43ff | ||
![]() |
1988dfc956 | ||
![]() |
e48b0ba61d | ||
![]() |
1a4b8662cf | ||
![]() |
e70be4f155 | ||
![]() |
6621f6ddb2 |
5
.changeset/rare-women-fly.md
Normal file
5
.changeset/rare-women-fly.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'mermaid': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
fix: Add escaped class literal name on namespace
|
5
.changeset/silver-eyes-build.md
Normal file
5
.changeset/silver-eyes-build.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'mermaid': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
chore: migrate to class-based ArchitectureDB implementation
|
5
.changeset/vast-buses-see.md
Normal file
5
.changeset/vast-buses-see.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'mermaid': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
chore: Update packet diagram to use new class-based database structure
|
2
.github/workflows/e2e-timings.yml
vendored
2
.github/workflows/e2e-timings.yml
vendored
@@ -58,7 +58,7 @@ jobs:
|
|||||||
echo "EOF" >> $GITHUB_OUTPUT
|
echo "EOF" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Commit and create pull request
|
- name: Commit and create pull request
|
||||||
uses: peter-evans/create-pull-request@2e50522bdf313efe32e5628afead9048374012ed
|
uses: peter-evans/create-pull-request@07cbaebb4bfc9c5d7db426ea5a5f585df29dd0a0
|
||||||
with:
|
with:
|
||||||
add-paths: |
|
add-paths: |
|
||||||
cypress/timings.json
|
cypress/timings.json
|
||||||
|
@@ -512,4 +512,17 @@ describe('Class diagram', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should handle backticks for namespace and class names', () => {
|
||||||
|
imgSnapshotTest(
|
||||||
|
`
|
||||||
|
classDiagram
|
||||||
|
namespace \`A::B\` {
|
||||||
|
class \`IPC::Sender\`
|
||||||
|
}
|
||||||
|
RenderProcessHost --|> \`IPC::Sender\`
|
||||||
|
`,
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -104,6 +104,7 @@ Blogging frameworks and platforms
|
|||||||
- [Mermaid](https://nextra.site/docs/guide/mermaid)
|
- [Mermaid](https://nextra.site/docs/guide/mermaid)
|
||||||
- [WordPress](https://wordpress.org)
|
- [WordPress](https://wordpress.org)
|
||||||
- [MerPRess](https://wordpress.org/plugins/merpress/)
|
- [MerPRess](https://wordpress.org/plugins/merpress/)
|
||||||
|
- [WP Documentation](https://wordpress.org/themes/wp-documentation/)
|
||||||
|
|
||||||
### CMS/ECM
|
### CMS/ECM
|
||||||
|
|
||||||
|
@@ -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');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -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,333 @@ 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 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[rhsId] === 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 keyof ArchitectureDiagramConfig>(
|
||||||
field: T
|
// field: T
|
||||||
): Required<ArchitectureDiagramConfig>[T] {
|
// ): Required<ArchitectureDiagramConfig>[T] {
|
||||||
return getConfig()[field];
|
// return db.getConfig()[field];
|
||||||
}
|
// }
|
||||||
|
@@ -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,
|
||||||
};
|
};
|
||||||
|
@@ -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);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@@ -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',
|
||||||
});
|
});
|
||||||
@@ -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 };
|
||||||
|
@@ -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
|
||||||
|
@@ -15,4 +15,12 @@ describe('class diagram', function () {
|
|||||||
expect(() => parser.parse(`classDiagram\nnamespace ${prop} {\n\tclass A\n}`)).not.toThrow();
|
expect(() => parser.parse(`classDiagram\nnamespace ${prop} {\n\tclass A\n}`)).not.toThrow();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('backtick escaping', function () {
|
||||||
|
it('should handle backtick-quoted namespace names', function () {
|
||||||
|
expect(() =>
|
||||||
|
parser.parse(`classDiagram\nnamespace \`A::B\` {\n\tclass \`IPC::Sender\`\n}`)
|
||||||
|
).not.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -242,6 +242,7 @@ classLabel
|
|||||||
|
|
||||||
namespaceName
|
namespaceName
|
||||||
: alphaNumToken { $$=$1; }
|
: alphaNumToken { $$=$1; }
|
||||||
|
| classLiteralName { $$=$1; }
|
||||||
| alphaNumToken DOT namespaceName { $$=$1+'.'+$3; }
|
| alphaNumToken DOT namespaceName { $$=$1+'.'+$3; }
|
||||||
| alphaNumToken namespaceName { $$=$1+$2; }
|
| alphaNumToken namespaceName { $$=$1+$2; }
|
||||||
;
|
;
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import { getConfig as commonGetConfig } from '../../config.js';
|
import { getConfig as commonGetConfig } from '../../config.js';
|
||||||
import type { PacketDiagramConfig } from '../../config.type.js';
|
import type { PacketDiagramConfig } from '../../config.type.js';
|
||||||
import DEFAULT_CONFIG from '../../defaultConfig.js';
|
import DEFAULT_CONFIG from '../../defaultConfig.js';
|
||||||
|
import type { DiagramDB } from '../../diagram-api/types.js';
|
||||||
import { cleanAndMerge } from '../../utils.js';
|
import { cleanAndMerge } from '../../utils.js';
|
||||||
import {
|
import {
|
||||||
clear as commonClear,
|
clear as commonClear,
|
||||||
@@ -11,49 +12,42 @@ import {
|
|||||||
setAccTitle,
|
setAccTitle,
|
||||||
setDiagramTitle,
|
setDiagramTitle,
|
||||||
} from '../common/commonDb.js';
|
} from '../common/commonDb.js';
|
||||||
import type { PacketDB, PacketData, PacketWord } from './types.js';
|
import type { PacketWord } from './types.js';
|
||||||
|
|
||||||
const defaultPacketData: PacketData = {
|
|
||||||
packet: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
let data: PacketData = structuredClone(defaultPacketData);
|
|
||||||
|
|
||||||
const DEFAULT_PACKET_CONFIG: Required<PacketDiagramConfig> = DEFAULT_CONFIG.packet;
|
const DEFAULT_PACKET_CONFIG: Required<PacketDiagramConfig> = DEFAULT_CONFIG.packet;
|
||||||
|
|
||||||
const getConfig = (): Required<PacketDiagramConfig> => {
|
export class PacketDB implements DiagramDB {
|
||||||
const config = cleanAndMerge({
|
private packet: PacketWord[] = [];
|
||||||
...DEFAULT_PACKET_CONFIG,
|
|
||||||
...commonGetConfig().packet,
|
public getConfig() {
|
||||||
});
|
const config = cleanAndMerge({
|
||||||
if (config.showBits) {
|
...DEFAULT_PACKET_CONFIG,
|
||||||
config.paddingY += 10;
|
...commonGetConfig().packet,
|
||||||
|
});
|
||||||
|
if (config.showBits) {
|
||||||
|
config.paddingY += 10;
|
||||||
|
}
|
||||||
|
return config;
|
||||||
}
|
}
|
||||||
return config;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getPacket = (): PacketWord[] => data.packet;
|
public getPacket() {
|
||||||
|
return this.packet;
|
||||||
const pushWord = (word: PacketWord) => {
|
|
||||||
if (word.length > 0) {
|
|
||||||
data.packet.push(word);
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const clear = () => {
|
public pushWord(word: PacketWord) {
|
||||||
commonClear();
|
if (word.length > 0) {
|
||||||
data = structuredClone(defaultPacketData);
|
this.packet.push(word);
|
||||||
};
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const db: PacketDB = {
|
public clear() {
|
||||||
pushWord,
|
commonClear();
|
||||||
getPacket,
|
this.packet = [];
|
||||||
getConfig,
|
}
|
||||||
clear,
|
|
||||||
setAccTitle,
|
public setAccTitle = setAccTitle;
|
||||||
getAccTitle,
|
public getAccTitle = getAccTitle;
|
||||||
setDiagramTitle,
|
public setDiagramTitle = setDiagramTitle;
|
||||||
getDiagramTitle,
|
public getDiagramTitle = getDiagramTitle;
|
||||||
getAccDescription,
|
public getAccDescription = getAccDescription;
|
||||||
setAccDescription,
|
public setAccDescription = setAccDescription;
|
||||||
};
|
}
|
||||||
|
@@ -1,12 +1,14 @@
|
|||||||
import type { DiagramDefinition } from '../../diagram-api/types.js';
|
import type { DiagramDefinition } from '../../diagram-api/types.js';
|
||||||
import { db } from './db.js';
|
import { PacketDB } from './db.js';
|
||||||
import { parser } from './parser.js';
|
import { parser } from './parser.js';
|
||||||
import { renderer } from './renderer.js';
|
import { renderer } from './renderer.js';
|
||||||
import { styles } from './styles.js';
|
import { styles } from './styles.js';
|
||||||
|
|
||||||
export const diagram: DiagramDefinition = {
|
export const diagram: DiagramDefinition = {
|
||||||
parser,
|
parser,
|
||||||
db,
|
get db() {
|
||||||
|
return new PacketDB();
|
||||||
|
},
|
||||||
renderer,
|
renderer,
|
||||||
styles,
|
styles,
|
||||||
};
|
};
|
||||||
|
@@ -1,24 +1,26 @@
|
|||||||
import { it, describe, expect } from 'vitest';
|
import { it, describe, expect } from 'vitest';
|
||||||
import { db } from './db.js';
|
import { PacketDB } from './db.js';
|
||||||
import { parser } from './parser.js';
|
import { parser } from './parser.js';
|
||||||
|
|
||||||
const { clear, getPacket, getDiagramTitle, getAccTitle, getAccDescription } = db;
|
|
||||||
|
|
||||||
describe('packet diagrams', () => {
|
describe('packet diagrams', () => {
|
||||||
|
let db: PacketDB;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
clear();
|
db = new PacketDB();
|
||||||
|
if (parser.parser) {
|
||||||
|
parser.parser.yy = db;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle a packet-beta definition', async () => {
|
it('should handle a packet-beta definition', async () => {
|
||||||
const str = `packet-beta`;
|
const str = `packet-beta`;
|
||||||
await expect(parser.parse(str)).resolves.not.toThrow();
|
await expect(parser.parse(str)).resolves.not.toThrow();
|
||||||
expect(getPacket()).toMatchInlineSnapshot('[]');
|
expect(db.getPacket()).toMatchInlineSnapshot('[]');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle a packet definition', async () => {
|
it('should handle a packet definition', async () => {
|
||||||
const str = `packet`;
|
const str = `packet`;
|
||||||
await expect(parser.parse(str)).resolves.not.toThrow();
|
await expect(parser.parse(str)).resolves.not.toThrow();
|
||||||
expect(getPacket()).toMatchInlineSnapshot('[]');
|
expect(db.getPacket()).toMatchInlineSnapshot('[]');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle diagram with data and title', async () => {
|
it('should handle diagram with data and title', async () => {
|
||||||
@@ -29,10 +31,10 @@ describe('packet diagrams', () => {
|
|||||||
0-10: "test"
|
0-10: "test"
|
||||||
`;
|
`;
|
||||||
await expect(parser.parse(str)).resolves.not.toThrow();
|
await expect(parser.parse(str)).resolves.not.toThrow();
|
||||||
expect(getDiagramTitle()).toMatchInlineSnapshot('"Packet diagram"');
|
expect(db.getDiagramTitle()).toMatchInlineSnapshot('"Packet diagram"');
|
||||||
expect(getAccTitle()).toMatchInlineSnapshot('"Packet accTitle"');
|
expect(db.getAccTitle()).toMatchInlineSnapshot('"Packet accTitle"');
|
||||||
expect(getAccDescription()).toMatchInlineSnapshot('"Packet accDescription"');
|
expect(db.getAccDescription()).toMatchInlineSnapshot('"Packet accDescription"');
|
||||||
expect(getPacket()).toMatchInlineSnapshot(`
|
expect(db.getPacket()).toMatchInlineSnapshot(`
|
||||||
[
|
[
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
@@ -52,7 +54,7 @@ describe('packet diagrams', () => {
|
|||||||
11: "single"
|
11: "single"
|
||||||
`;
|
`;
|
||||||
await expect(parser.parse(str)).resolves.not.toThrow();
|
await expect(parser.parse(str)).resolves.not.toThrow();
|
||||||
expect(getPacket()).toMatchInlineSnapshot(`
|
expect(db.getPacket()).toMatchInlineSnapshot(`
|
||||||
[
|
[
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
@@ -78,7 +80,7 @@ describe('packet diagrams', () => {
|
|||||||
+16: "word"
|
+16: "word"
|
||||||
`;
|
`;
|
||||||
await expect(parser.parse(str)).resolves.not.toThrow();
|
await expect(parser.parse(str)).resolves.not.toThrow();
|
||||||
expect(getPacket()).toMatchInlineSnapshot(`
|
expect(db.getPacket()).toMatchInlineSnapshot(`
|
||||||
[
|
[
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
@@ -104,7 +106,7 @@ describe('packet diagrams', () => {
|
|||||||
+16: "word"
|
+16: "word"
|
||||||
`;
|
`;
|
||||||
await expect(parser.parse(str)).resolves.not.toThrow();
|
await expect(parser.parse(str)).resolves.not.toThrow();
|
||||||
expect(getPacket()).toMatchInlineSnapshot(`
|
expect(db.getPacket()).toMatchInlineSnapshot(`
|
||||||
[
|
[
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
@@ -130,7 +132,7 @@ describe('packet diagrams', () => {
|
|||||||
11-90: "multiple"
|
11-90: "multiple"
|
||||||
`;
|
`;
|
||||||
await expect(parser.parse(str)).resolves.not.toThrow();
|
await expect(parser.parse(str)).resolves.not.toThrow();
|
||||||
expect(getPacket()).toMatchInlineSnapshot(`
|
expect(db.getPacket()).toMatchInlineSnapshot(`
|
||||||
[
|
[
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
@@ -172,7 +174,7 @@ describe('packet diagrams', () => {
|
|||||||
17-63: "multiple"
|
17-63: "multiple"
|
||||||
`;
|
`;
|
||||||
await expect(parser.parse(str)).resolves.not.toThrow();
|
await expect(parser.parse(str)).resolves.not.toThrow();
|
||||||
expect(getPacket()).toMatchInlineSnapshot(`
|
expect(db.getPacket()).toMatchInlineSnapshot(`
|
||||||
[
|
[
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
|
@@ -3,12 +3,12 @@ import { parse } from '@mermaid-js/parser';
|
|||||||
import type { ParserDefinition } from '../../diagram-api/types.js';
|
import type { ParserDefinition } from '../../diagram-api/types.js';
|
||||||
import { log } from '../../logger.js';
|
import { log } from '../../logger.js';
|
||||||
import { populateCommonDb } from '../common/populateCommonDb.js';
|
import { populateCommonDb } from '../common/populateCommonDb.js';
|
||||||
import { db } from './db.js';
|
import { PacketDB } from './db.js';
|
||||||
import type { PacketBlock, PacketWord } from './types.js';
|
import type { PacketBlock, PacketWord } from './types.js';
|
||||||
|
|
||||||
const maxPacketSize = 10_000;
|
const maxPacketSize = 10_000;
|
||||||
|
|
||||||
const populate = (ast: Packet) => {
|
const populate = (ast: Packet, db: PacketDB) => {
|
||||||
populateCommonDb(ast, db);
|
populateCommonDb(ast, db);
|
||||||
let lastBit = -1;
|
let lastBit = -1;
|
||||||
let word: PacketWord = [];
|
let word: PacketWord = [];
|
||||||
@@ -91,9 +91,17 @@ const getNextFittingBlock = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const parser: ParserDefinition = {
|
export const parser: ParserDefinition = {
|
||||||
|
// @ts-expect-error - PacketDB is not assignable to DiagramDB
|
||||||
|
parser: { yy: undefined },
|
||||||
parse: async (input: string): Promise<void> => {
|
parse: async (input: string): Promise<void> => {
|
||||||
const ast: Packet = await parse('packet', input);
|
const ast: Packet = await parse('packet', input);
|
||||||
|
const db = parser.parser?.yy;
|
||||||
|
if (!(db instanceof PacketDB)) {
|
||||||
|
throw new Error(
|
||||||
|
'parser.parser?.yy was not a PacketDB. This is due to a bug within Mermaid, please report this issue at https://github.com/mermaid-js/mermaid/issues.'
|
||||||
|
);
|
||||||
|
}
|
||||||
log.debug(ast);
|
log.debug(ast);
|
||||||
populate(ast);
|
populate(ast, db);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@@ -99,6 +99,7 @@ Blogging frameworks and platforms
|
|||||||
- [Mermaid](https://nextra.site/docs/guide/mermaid)
|
- [Mermaid](https://nextra.site/docs/guide/mermaid)
|
||||||
- [WordPress](https://wordpress.org)
|
- [WordPress](https://wordpress.org)
|
||||||
- [MerPRess](https://wordpress.org/plugins/merpress/)
|
- [MerPRess](https://wordpress.org/plugins/merpress/)
|
||||||
|
- [WP Documentation](https://wordpress.org/themes/wp-documentation/)
|
||||||
|
|
||||||
### CMS/ECM
|
### CMS/ECM
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user