diff --git a/.changeset/light-flowers-judge.md b/.changeset/light-flowers-judge.md new file mode 100644 index 000000000..6378a9b0a --- /dev/null +++ b/.changeset/light-flowers-judge.md @@ -0,0 +1,5 @@ +--- +'mermaid': patch +--- + +fix: Make flowchart elk detector regex match less greedy diff --git a/.changeset/rare-women-fly.md b/.changeset/rare-women-fly.md new file mode 100644 index 000000000..46d08dd10 --- /dev/null +++ b/.changeset/rare-women-fly.md @@ -0,0 +1,5 @@ +--- +'mermaid': patch +--- + +fix: Add escaped class literal name on namespace diff --git a/.changeset/silver-eyes-build.md b/.changeset/silver-eyes-build.md new file mode 100644 index 000000000..76f0a0125 --- /dev/null +++ b/.changeset/silver-eyes-build.md @@ -0,0 +1,5 @@ +--- +'mermaid': patch +--- + +chore: migrate to class-based ArchitectureDB implementation diff --git a/.changeset/vast-buses-see.md b/.changeset/vast-buses-see.md new file mode 100644 index 000000000..fc2a0e6c6 --- /dev/null +++ b/.changeset/vast-buses-see.md @@ -0,0 +1,5 @@ +--- +'mermaid': patch +--- + +chore: Update packet diagram to use new class-based database structure diff --git a/.github/workflows/e2e-timings.yml b/.github/workflows/e2e-timings.yml index 5040bb8e4..ef009fa2b 100644 --- a/.github/workflows/e2e-timings.yml +++ b/.github/workflows/e2e-timings.yml @@ -58,7 +58,7 @@ jobs: echo "EOF" >> $GITHUB_OUTPUT - name: Commit and create pull request - uses: peter-evans/create-pull-request@2e50522bdf313efe32e5628afead9048374012ed + uses: peter-evans/create-pull-request@07cbaebb4bfc9c5d7db426ea5a5f585df29dd0a0 with: add-paths: | cypress/timings.json diff --git a/cypress/integration/rendering/classDiagram.spec.js b/cypress/integration/rendering/classDiagram.spec.js index e7d201b5d..bd2a96b34 100644 --- a/cypress/integration/rendering/classDiagram.spec.js +++ b/cypress/integration/rendering/classDiagram.spec.js @@ -495,4 +495,34 @@ describe('Class diagram', () => { cy.get('a').should('have.attr', 'target', '_blank').should('have.attr', 'rel', 'noopener'); }); }); + + describe('Include char sequence "graph" in text (#6795)', () => { + it('has a label with char sequence "graph"', () => { + imgSnapshotTest( + ` + classDiagram + class Person { + +String name + -Int id + #double age + +Text demographicProfile + } + `, + { flowchart: { defaultRenderer: 'elk' } } + ); + }); + }); + + it('should handle backticks for namespace and class names', () => { + imgSnapshotTest( + ` + classDiagram + namespace \`A::B\` { + class \`IPC::Sender\` + } + RenderProcessHost --|> \`IPC::Sender\` + `, + {} + ); + }); }); diff --git a/cypress/integration/rendering/erDiagram.spec.js b/cypress/integration/rendering/erDiagram.spec.js index cbfec8218..8f6193f96 100644 --- a/cypress/integration/rendering/erDiagram.spec.js +++ b/cypress/integration/rendering/erDiagram.spec.js @@ -354,4 +354,19 @@ ORDER ||--|{ LINE-ITEM : contains { logLevel: 1 } ); }); + + describe('Include char sequence "graph" in text (#6795)', () => { + it('has a label with char sequence "graph"', () => { + imgSnapshotTest( + ` + erDiagram + p[Photograph] { + varchar(12) jobId + date dateCreated + } + `, + { flowchart: { defaultRenderer: 'elk' } } + ); + }); + }); }); diff --git a/cypress/integration/rendering/mindmap.spec.ts b/cypress/integration/rendering/mindmap.spec.ts index 731f861ee..d76e58c56 100644 --- a/cypress/integration/rendering/mindmap.spec.ts +++ b/cypress/integration/rendering/mindmap.spec.ts @@ -246,5 +246,22 @@ Word!\`] ); }); }); + describe('Include char sequence "graph" in text (#6795)', () => { + it('has a label with char sequence "graph"', () => { + imgSnapshotTest( + ` + mindmap + root + Photograph + Waterfall + Landscape + Geography + Mountains + Rocks + `, + { flowchart: { defaultRenderer: 'elk' } } + ); + }); + }); /* The end */ }); diff --git a/docs/ecosystem/integrations-community.md b/docs/ecosystem/integrations-community.md index cc5dbad1a..e86abfd81 100644 --- a/docs/ecosystem/integrations-community.md +++ b/docs/ecosystem/integrations-community.md @@ -104,6 +104,7 @@ Blogging frameworks and platforms - [Mermaid](https://nextra.site/docs/guide/mermaid) - [WordPress](https://wordpress.org) - [MerPRess](https://wordpress.org/plugins/merpress/) + - [WP Documentation](https://wordpress.org/themes/wp-documentation/) ### CMS/ECM diff --git a/packages/mermaid/src/diagram-api/diagram-orchestration.spec.ts b/packages/mermaid/src/diagram-api/diagram-orchestration.spec.ts index 3ec2d861f..f4f7c58bc 100644 --- a/packages/mermaid/src/diagram-api/diagram-orchestration.spec.ts +++ b/packages/mermaid/src/diagram-api/diagram-orchestration.spec.ts @@ -78,5 +78,41 @@ describe('diagram-orchestration', () => { flowchart: 1 "pie" pie: 2 "pie"`) ).toBe('pie'); }); + + it('should detect proper diagram when defaultRenderer is elk for flowchart', () => { + expect( + detectType('mindmap\n root\n Photograph\n Waterfall', { + flowchart: { defaultRenderer: 'elk' }, + }) + ).toBe('mindmap'); + expect( + detectType( + ` + classDiagram + class Person { + +String name + -Int id + #double age + +Text demographicProfile + } + `, + { flowchart: { defaultRenderer: 'elk' } } + ) + ).toBe('class'); + expect( + detectType( + ` + erDiagram + p[Photograph] { + varchar(12) jobId + date dateCreated + } + `, + { + flowchart: { defaultRenderer: 'elk' }, + } + ) + ).toBe('er'); + }); }); }); diff --git a/packages/mermaid/src/diagrams/architecture/architecture.spec.ts b/packages/mermaid/src/diagrams/architecture/architecture.spec.ts index d0405d025..49cc18a07 100644 --- a/packages/mermaid/src/diagrams/architecture/architecture.spec.ts +++ b/packages/mermaid/src/diagrams/architecture/architecture.spec.ts @@ -1,21 +1,12 @@ import { it, describe, expect } from 'vitest'; -import { db } from './architectureDb.js'; import { parser } from './architectureParser.js'; - -const { - clear, - getDiagramTitle, - getAccTitle, - getAccDescription, - getServices, - getGroups, - getEdges, - getJunctions, -} = db; - +import { ArchitectureDB } from './architectureDb.js'; describe('architecture diagrams', () => { + let db: ArchitectureDB; beforeEach(() => { - clear(); + db = new ArchitectureDB(); + // @ts-expect-error since type is set to undefined we will have error + parser.parser?.yy = db; }); describe('architecture diagram definitions', () => { @@ -36,7 +27,7 @@ describe('architecture diagrams', () => { it('should handle title on the first line', async () => { const str = `architecture-beta title Simple Architecture Diagram`; await expect(parser.parse(str)).resolves.not.toThrow(); - expect(getDiagramTitle()).toBe('Simple Architecture Diagram'); + expect(db.getDiagramTitle()).toBe('Simple Architecture Diagram'); }); it('should handle title on another line', async () => { @@ -44,7 +35,7 @@ describe('architecture diagrams', () => { title Simple Architecture Diagram `; await expect(parser.parse(str)).resolves.not.toThrow(); - expect(getDiagramTitle()).toBe('Simple Architecture Diagram'); + expect(db.getDiagramTitle()).toBe('Simple Architecture Diagram'); }); it('should handle accessibility title and description', async () => { @@ -53,8 +44,8 @@ describe('architecture diagrams', () => { accDescr: Accessibility Description `; await expect(parser.parse(str)).resolves.not.toThrow(); - expect(getAccTitle()).toBe('Accessibility Title'); - expect(getAccDescription()).toBe('Accessibility Description'); + expect(db.getAccTitle()).toBe('Accessibility Title'); + expect(db.getAccDescription()).toBe('Accessibility Description'); }); it('should handle multiline accessibility description', async () => { @@ -64,7 +55,7 @@ describe('architecture diagrams', () => { } `; await expect(parser.parse(str)).resolves.not.toThrow(); - expect(getAccDescription()).toBe('Accessibility Description'); + expect(db.getAccDescription()).toBe('Accessibility Description'); }); }); }); diff --git a/packages/mermaid/src/diagrams/architecture/architectureDb.ts b/packages/mermaid/src/diagrams/architecture/architectureDb.ts index c7bd64e21..4764671e8 100644 --- a/packages/mermaid/src/diagrams/architecture/architectureDb.ts +++ b/packages/mermaid/src/diagrams/architecture/architectureDb.ts @@ -1,8 +1,9 @@ +import { getConfig as commonGetConfig } from '../../config.js'; import type { ArchitectureDiagramConfig } from '../../config.type.js'; import DEFAULT_CONFIG from '../../defaultConfig.js'; -import { getConfig as commonGetConfig } from '../../config.js'; +import type { DiagramDB } from '../../diagram-api/types.js'; import type { D3Element } from '../../types.js'; -import { ImperativeState } from '../../utils/imperativeState.js'; +import { cleanAndMerge } from '../../utils.js'; import { clear as commonClear, getAccDescription, @@ -14,7 +15,6 @@ import { } from '../common/commonDb.js'; import type { ArchitectureAlignment, - ArchitectureDB, ArchitectureDirectionPair, ArchitectureDirectionPairMap, ArchitectureEdge, @@ -33,330 +33,333 @@ import { isArchitectureService, shiftPositionByArchitectureDirectionPair, } from './architectureTypes.js'; -import { cleanAndMerge } from '../../utils.js'; const DEFAULT_ARCHITECTURE_CONFIG: Required = DEFAULT_CONFIG.architecture; +export class ArchitectureDB implements DiagramDB { + private nodes: Record = {}; + private groups: Record = {}; + private edges: ArchitectureEdge[] = []; + private registeredIds: Record = {}; + private dataStructures?: ArchitectureState['dataStructures']; + private elements: Record = {}; -const state = new ImperativeState(() => ({ - nodes: {}, - groups: {}, - edges: [], - registeredIds: {}, - config: DEFAULT_ARCHITECTURE_CONFIG, - dataStructures: undefined, - elements: {}, -})); - -const clear = (): void => { - state.reset(); - commonClear(); -}; - -const addService = function ({ - id, - icon, - in: parent, - title, - iconText, -}: Omit) { - if (state.records.registeredIds[id] !== undefined) { - throw new Error( - `The service id [${id}] is already in use by another ${state.records.registeredIds[id]}` - ); - } - if (parent !== undefined) { - if (id === parent) { - throw new Error(`The service [${id}] cannot be placed within itself`); - } - if (state.records.registeredIds[parent] === undefined) { - throw new Error( - `The service [${id}]'s parent does not exist. Please make sure the parent is created before this service` - ); - } - if (state.records.registeredIds[parent] === 'node') { - throw new Error(`The service [${id}]'s parent is not a group`); - } + constructor() { + this.clear(); } - state.records.registeredIds[id] = 'node'; + public clear(): void { + this.nodes = {}; + this.groups = {}; + this.edges = []; + this.registeredIds = {}; + this.dataStructures = undefined; + this.elements = {}; + commonClear(); + } - state.records.nodes[id] = { + public addService({ id, - type: 'service', icon, + in: parent, + title, iconText, - title, - edges: [], - in: parent, - }; -}; - -const getServices = (): ArchitectureService[] => - Object.values(state.records.nodes).filter(isArchitectureService); - -const addJunction = function ({ id, in: parent }: Omit) { - state.records.registeredIds[id] = 'node'; - - state.records.nodes[id] = { - id, - type: 'junction', - edges: [], - in: parent, - }; -}; - -const getJunctions = (): ArchitectureJunction[] => - Object.values(state.records.nodes).filter(isArchitectureJunction); - -const getNodes = (): ArchitectureNode[] => Object.values(state.records.nodes); - -const getNode = (id: string): ArchitectureNode | null => state.records.nodes[id]; - -const addGroup = function ({ id, icon, in: parent, title }: ArchitectureGroup) { - if (state.records.registeredIds[id] !== undefined) { - throw new Error( - `The group id [${id}] is already in use by another ${state.records.registeredIds[id]}` - ); - } - if (parent !== undefined) { - if (id === parent) { - throw new Error(`The group [${id}] cannot be placed within itself`); - } - if (state.records.registeredIds[parent] === undefined) { + }: Omit): void { + if (this.registeredIds[id] !== undefined) { throw new Error( - `The group [${id}]'s parent does not exist. Please make sure the parent is created before this group` + `The service id [${id}] is already in use by another ${this.registeredIds[id]}` ); } - if (state.records.registeredIds[parent] === 'node') { - throw new Error(`The group [${id}]'s parent is not a group`); + if (parent !== undefined) { + if (id === parent) { + throw new Error(`The service [${id}] cannot be placed within itself`); + } + if (this.registeredIds[parent] === undefined) { + throw new Error( + `The service [${id}]'s parent does not exist. Please make sure the parent is created before this service` + ); + } + if (this.registeredIds[parent] === 'node') { + throw new Error(`The service [${id}]'s parent is not a group`); + } } + + this.registeredIds[id] = 'node'; + + this.nodes[id] = { + id, + type: 'service', + icon, + iconText, + title, + edges: [], + in: parent, + }; } - state.records.registeredIds[id] = 'group'; - - state.records.groups[id] = { - id, - icon, - title, - in: parent, - }; -}; -const getGroups = (): ArchitectureGroup[] => { - return Object.values(state.records.groups); -}; - -const addEdge = function ({ - lhsId, - rhsId, - lhsDir, - rhsDir, - lhsInto, - rhsInto, - lhsGroup, - rhsGroup, - title, -}: ArchitectureEdge) { - if (!isArchitectureDirection(lhsDir)) { - throw new Error( - `Invalid direction given for left hand side of edge ${lhsId}--${rhsId}. Expected (L,R,T,B) got ${lhsDir}` - ); - } - if (!isArchitectureDirection(rhsDir)) { - throw new Error( - `Invalid direction given for right hand side of edge ${lhsId}--${rhsId}. Expected (L,R,T,B) got ${rhsDir}` - ); + public getServices(): ArchitectureService[] { + return Object.values(this.nodes).filter(isArchitectureService); } - if (state.records.nodes[lhsId] === undefined && state.records.groups[lhsId] === undefined) { - throw new Error( - `The left-hand id [${lhsId}] does not yet exist. Please create the service/group before declaring an edge to it.` - ); - } - if (state.records.nodes[rhsId] === undefined && state.records.groups[lhsId] === undefined) { - throw new Error( - `The right-hand id [${rhsId}] does not yet exist. Please create the service/group before declaring an edge to it.` - ); + public addJunction({ id, in: parent }: Omit): void { + this.registeredIds[id] = 'node'; + + this.nodes[id] = { + id, + type: 'junction', + edges: [], + in: parent, + }; } - const lhsGroupId = state.records.nodes[lhsId].in; - const rhsGroupId = state.records.nodes[rhsId].in; - if (lhsGroup && lhsGroupId && rhsGroupId && lhsGroupId == rhsGroupId) { - throw new Error( - `The left-hand id [${lhsId}] is modified to traverse the group boundary, but the edge does not pass through two groups.` - ); - } - if (rhsGroup && lhsGroupId && rhsGroupId && lhsGroupId == rhsGroupId) { - throw new Error( - `The right-hand id [${rhsId}] is modified to traverse the group boundary, but the edge does not pass through two groups.` - ); + public getJunctions(): ArchitectureJunction[] { + return Object.values(this.nodes).filter(isArchitectureJunction); } - const edge = { + public getNodes(): ArchitectureNode[] { + return Object.values(this.nodes); + } + + public getNode(id: string): ArchitectureNode | null { + return this.nodes[id] ?? null; + } + + public addGroup({ id, icon, in: parent, title }: ArchitectureGroup): void { + if (this.registeredIds?.[id] !== undefined) { + throw new Error( + `The group id [${id}] is already in use by another ${this.registeredIds[id]}` + ); + } + if (parent !== undefined) { + if (id === parent) { + throw new Error(`The group [${id}] cannot be placed within itself`); + } + if (this.registeredIds?.[parent] === undefined) { + throw new Error( + `The group [${id}]'s parent does not exist. Please make sure the parent is created before this group` + ); + } + if (this.registeredIds?.[parent] === 'node') { + throw new Error(`The group [${id}]'s parent is not a group`); + } + } + + this.registeredIds[id] = 'group'; + + this.groups[id] = { + id, + icon, + title, + in: parent, + }; + } + public getGroups(): ArchitectureGroup[] { + return Object.values(this.groups); + } + public addEdge({ lhsId, - lhsDir, - lhsInto, - lhsGroup, rhsId, + lhsDir, rhsDir, + lhsInto, rhsInto, + lhsGroup, rhsGroup, title, - }; - - state.records.edges.push(edge); - if (state.records.nodes[lhsId] && state.records.nodes[rhsId]) { - state.records.nodes[lhsId].edges.push(state.records.edges[state.records.edges.length - 1]); - state.records.nodes[rhsId].edges.push(state.records.edges[state.records.edges.length - 1]); - } -}; - -const getEdges = (): ArchitectureEdge[] => state.records.edges; - -/** - * Returns the current diagram's adjacency list, spatial map, & group alignments. - * If they have not been created, run the algorithms to generate them. - * @returns - */ -const getDataStructures = () => { - if (state.records.dataStructures === undefined) { - // Tracks how groups are aligned with one another. Generated while creating the adj list - const groupAlignments: Record< - string, - Record> - > = {}; - - // Create an adjacency list of the diagram to perform BFS on - // Outer reduce applied on all services - // Inner reduce applied on the edges for a service - const adjList = Object.entries(state.records.nodes).reduce< - Record - >((prevOuter, [id, service]) => { - prevOuter[id] = service.edges.reduce((prevInner, edge) => { - // track the direction groups connect to one another - const lhsGroupId = getNode(edge.lhsId)?.in; - const rhsGroupId = getNode(edge.rhsId)?.in; - if (lhsGroupId && rhsGroupId && lhsGroupId !== rhsGroupId) { - const alignment = getArchitectureDirectionAlignment(edge.lhsDir, edge.rhsDir); - if (alignment !== 'bend') { - groupAlignments[lhsGroupId] ??= {}; - groupAlignments[lhsGroupId][rhsGroupId] = alignment; - groupAlignments[rhsGroupId] ??= {}; - groupAlignments[rhsGroupId][lhsGroupId] = alignment; - } - } - - if (edge.lhsId === id) { - // source is LHS - const pair = getArchitectureDirectionPair(edge.lhsDir, edge.rhsDir); - if (pair) { - prevInner[pair] = edge.rhsId; - } - } else { - // source is RHS - const pair = getArchitectureDirectionPair(edge.rhsDir, edge.lhsDir); - if (pair) { - prevInner[pair] = edge.lhsId; - } - } - return prevInner; - }, {}); - return prevOuter; - }, {}); - - // Configuration for the initial pass of BFS - const firstId = Object.keys(adjList)[0]; - const visited = { [firstId]: 1 }; - // If a key is present in this object, it has not been visited - const notVisited = Object.keys(adjList).reduce( - (prev, id) => (id === firstId ? prev : { ...prev, [id]: 1 }), - {} as Record - ); - - // Perform BFS on the adjacency list - const BFS = (startingId: string): ArchitectureSpatialMap => { - const spatialMap = { [startingId]: [0, 0] }; - const queue = [startingId]; - while (queue.length > 0) { - const id = queue.shift(); - if (id) { - visited[id] = 1; - delete notVisited[id]; - const adj = adjList[id]; - const [posX, posY] = spatialMap[id]; - Object.entries(adj).forEach(([dir, rhsId]) => { - if (!visited[rhsId]) { - spatialMap[rhsId] = shiftPositionByArchitectureDirectionPair( - [posX, posY], - dir as ArchitectureDirectionPair - ); - queue.push(rhsId); - } - }); - } - } - return spatialMap; - }; - const spatialMaps = [BFS(firstId)]; - - // If our diagram is disconnected, keep adding additional spatial maps until all disconnected graphs have been found - while (Object.keys(notVisited).length > 0) { - spatialMaps.push(BFS(Object.keys(notVisited)[0])); + }: ArchitectureEdge): void { + if (!isArchitectureDirection(lhsDir)) { + throw new Error( + `Invalid direction given for left hand side of edge ${lhsId}--${rhsId}. Expected (L,R,T,B) got ${String(lhsDir)}` + ); } - state.records.dataStructures = { - adjList, - spatialMaps, - groupAlignments, + if (!isArchitectureDirection(rhsDir)) { + throw new Error( + `Invalid direction given for right hand side of edge ${lhsId}--${rhsId}. Expected (L,R,T,B) got ${String(rhsDir)}` + ); + } + + if (this.nodes[lhsId] === undefined && this.groups[lhsId] === undefined) { + throw new Error( + `The left-hand id [${lhsId}] does not yet exist. Please create the service/group before declaring an edge to it.` + ); + } + if (this.nodes[rhsId] === undefined && this.groups[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) => { - state.records.elements[id] = element; -}; -const getElementById = (id: string) => state.records.elements[id]; + public getEdges(): ArchitectureEdge[] { + return this.edges; + } -const getConfig = (): Required => { - const config = cleanAndMerge({ - ...DEFAULT_ARCHITECTURE_CONFIG, - ...commonGetConfig().architecture, - }); - return config; -}; + /** + * Returns the current diagram's adjacency list, spatial map, & group alignments. + * If they have not been created, run the algorithms to generate them. + * @returns + */ + public getDataStructures() { + if (this.dataStructures === undefined) { + // Tracks how groups are aligned with one another. Generated while creating the adj list + const groupAlignments: Record< + string, + Record> + > = {}; -export const db: ArchitectureDB = { - clear, - setDiagramTitle, - getDiagramTitle, - setAccTitle, - getAccTitle, - setAccDescription, - getAccDescription, - getConfig, + // Create an adjacency list of the diagram to perform BFS on + // Outer reduce applied on all services + // Inner reduce applied on the edges for a service + const adjList = Object.entries(this.nodes).reduce< + Record + >((prevOuter, [id, service]) => { + prevOuter[id] = service.edges.reduce((prevInner, edge) => { + // track the direction groups connect to one another + const lhsGroupId = this.getNode(edge.lhsId)?.in; + const rhsGroupId = this.getNode(edge.rhsId)?.in; + if (lhsGroupId && rhsGroupId && lhsGroupId !== rhsGroupId) { + const alignment = getArchitectureDirectionAlignment(edge.lhsDir, edge.rhsDir); + if (alignment !== 'bend') { + groupAlignments[lhsGroupId] ??= {}; + groupAlignments[lhsGroupId][rhsGroupId] = alignment; + groupAlignments[rhsGroupId] ??= {}; + groupAlignments[rhsGroupId][lhsGroupId] = alignment; + } + } - addService, - getServices, - addJunction, - getJunctions, - getNodes, - getNode, - addGroup, - getGroups, - addEdge, - getEdges, - setElementForId, - getElementById, - getDataStructures, -}; + if (edge.lhsId === id) { + // source is LHS + const pair = getArchitectureDirectionPair(edge.lhsDir, edge.rhsDir); + if (pair) { + prevInner[pair] = edge.rhsId; + } + } else { + // source is RHS + const pair = getArchitectureDirectionPair(edge.rhsDir, edge.lhsDir); + if (pair) { + prevInner[pair] = edge.lhsId; + } + } + return prevInner; + }, {}); + return prevOuter; + }, {}); + + // Configuration for the initial pass of BFS + const firstId = Object.keys(adjList)[0]; + const visited = { [firstId]: 1 }; + // If a key is present in this object, it has not been visited + const notVisited = Object.keys(adjList).reduce( + (prev, id) => (id === firstId ? prev : { ...prev, [id]: 1 }), + {} as Record + ); + + // Perform BFS on the adjacency list + const BFS = (startingId: string): ArchitectureSpatialMap => { + const spatialMap = { [startingId]: [0, 0] }; + const queue = [startingId]; + while (queue.length > 0) { + const id = queue.shift(); + if (id) { + visited[id] = 1; + delete notVisited[id]; + const adj = adjList[id]; + const [posX, posY] = spatialMap[id]; + Object.entries(adj).forEach(([dir, rhsId]) => { + if (!visited[rhsId]) { + spatialMap[rhsId] = shiftPositionByArchitectureDirectionPair( + [posX, posY], + dir as ArchitectureDirectionPair + ); + queue.push(rhsId); + } + }); + } + } + return spatialMap; + }; + const spatialMaps = [BFS(firstId)]; + + // If our diagram is disconnected, keep adding additional spatial maps until all disconnected graphs have been found + while (Object.keys(notVisited).length > 0) { + spatialMaps.push(BFS(Object.keys(notVisited)[0])); + } + this.dataStructures = { + adjList, + spatialMaps, + groupAlignments, + }; + } + return this.dataStructures; + } + + public setElementForId(id: string, element: D3Element): void { + this.elements[id] = element; + } + + public getElementById(id: string): D3Element { + return this.elements[id]; + } + + public getConfig(): Required { + return cleanAndMerge({ + ...DEFAULT_ARCHITECTURE_CONFIG, + ...commonGetConfig().architecture, + }); + } + + public getConfigField( + field: T + ): Required[T] { + return this.getConfig()[field]; + } + + public setAccTitle = setAccTitle; + public getAccTitle = getAccTitle; + public setDiagramTitle = setDiagramTitle; + public getDiagramTitle = getDiagramTitle; + public getAccDescription = getAccDescription; + public setAccDescription = setAccDescription; +} /** * Typed wrapper for resolving an architecture diagram's config fields. Returns the default value if undefined * @param field - the config field to access * @returns */ -export function getConfigField( - field: T -): Required[T] { - return getConfig()[field]; -} +// export function getConfigField( +// field: T +// ): Required[T] { +// return db.getConfig()[field]; +// } diff --git a/packages/mermaid/src/diagrams/architecture/architectureDiagram.ts b/packages/mermaid/src/diagrams/architecture/architectureDiagram.ts index 82dacd3e1..1d390a3ab 100644 --- a/packages/mermaid/src/diagrams/architecture/architectureDiagram.ts +++ b/packages/mermaid/src/diagrams/architecture/architectureDiagram.ts @@ -1,12 +1,14 @@ import type { DiagramDefinition } from '../../diagram-api/types.js'; import { parser } from './architectureParser.js'; -import { db } from './architectureDb.js'; +import { ArchitectureDB } from './architectureDb.js'; import styles from './architectureStyles.js'; import { renderer } from './architectureRenderer.js'; export const diagram: DiagramDefinition = { parser, - db, + get db() { + return new ArchitectureDB(); + }, renderer, styles, }; diff --git a/packages/mermaid/src/diagrams/architecture/architectureParser.ts b/packages/mermaid/src/diagrams/architecture/architectureParser.ts index a7159d907..58820dad4 100644 --- a/packages/mermaid/src/diagrams/architecture/architectureParser.ts +++ b/packages/mermaid/src/diagrams/architecture/architectureParser.ts @@ -1,24 +1,33 @@ import type { Architecture } from '@mermaid-js/parser'; import { parse } from '@mermaid-js/parser'; -import { log } from '../../logger.js'; import type { ParserDefinition } from '../../diagram-api/types.js'; +import { log } from '../../logger.js'; import { populateCommonDb } from '../common/populateCommonDb.js'; -import type { ArchitectureDB } from './architectureTypes.js'; -import { db } from './architectureDb.js'; +import { ArchitectureDB } from './architectureDb.js'; const populateDb = (ast: Architecture, db: ArchitectureDB) => { populateCommonDb(ast, db); - ast.groups.map(db.addGroup); + ast.groups.map((group) => db.addGroup(group)); ast.services.map((service) => db.addService({ ...service, type: 'service' })); ast.junctions.map((service) => db.addJunction({ ...service, type: 'junction' })); // @ts-ignore TODO our parser guarantees the type is L/R/T/B and not string. How to change to union type? - ast.edges.map(db.addEdge); + ast.edges.map((edge) => db.addEdge(edge)); }; export const parser: ParserDefinition = { + parser: { + // @ts-expect-error - ArchitectureDB is not assignable to DiagramDB + yy: undefined, + }, parse: async (input: string): Promise => { const ast: Architecture = await parse('architecture', input); log.debug(ast); + const db = parser.parser?.yy; + if (!(db instanceof ArchitectureDB)) { + throw new Error( + 'parser.parser?.yy was not a ArchitectureDB. This is due to a bug within Mermaid, please report this issue at https://github.com/mermaid-js/mermaid/issues.' + ); + } populateDb(ast, db); }, }; diff --git a/packages/mermaid/src/diagrams/architecture/architectureRenderer.ts b/packages/mermaid/src/diagrams/architecture/architectureRenderer.ts index 1505b1950..b29567236 100644 --- a/packages/mermaid/src/diagrams/architecture/architectureRenderer.ts +++ b/packages/mermaid/src/diagrams/architecture/architectureRenderer.ts @@ -1,4 +1,3 @@ -import { registerIconPacks } from '../../rendering-util/icons.js'; import type { Position } from 'cytoscape'; import cytoscape from 'cytoscape'; import type { FcoseLayoutOptions } from 'cytoscape-fcose'; @@ -7,9 +6,10 @@ import { select } from 'd3'; import type { DrawDefinition, SVG } from '../../diagram-api/types.js'; import type { Diagram } from '../../Diagram.js'; import { log } from '../../logger.js'; +import { registerIconPacks } from '../../rendering-util/icons.js'; import { selectSvgElement } from '../../rendering-util/selectSvgElement.js'; import { setupGraphViewbox } from '../../setupGraphViewbox.js'; -import { getConfigField } from './architectureDb.js'; +import type { ArchitectureDB } from './architectureDb.js'; import { architectureIcons } from './architectureIcons.js'; import type { ArchitectureAlignment, @@ -22,7 +22,6 @@ import type { NodeSingularData, } from './architectureTypes.js'; import { - type ArchitectureDB, type ArchitectureDirection, type ArchitectureEdge, type ArchitectureGroup, @@ -44,7 +43,7 @@ registerIconPacks([ ]); cytoscape.use(fcose); -function addServices(services: ArchitectureService[], cy: cytoscape.Core) { +function addServices(services: ArchitectureService[], cy: cytoscape.Core, db: ArchitectureDB) { services.forEach((service) => { cy.add({ group: 'nodes', @@ -54,15 +53,15 @@ function addServices(services: ArchitectureService[], cy: cytoscape.Core) { icon: service.icon, label: service.title, parent: service.in, - width: getConfigField('iconSize'), - height: getConfigField('iconSize'), + width: db.getConfigField('iconSize'), + height: db.getConfigField('iconSize'), } as NodeSingularData, classes: 'node-service', }); }); } -function addJunctions(junctions: ArchitectureJunction[], cy: cytoscape.Core) { +function addJunctions(junctions: ArchitectureJunction[], cy: cytoscape.Core, db: ArchitectureDB) { junctions.forEach((junction) => { cy.add({ group: 'nodes', @@ -70,8 +69,8 @@ function addJunctions(junctions: ArchitectureJunction[], cy: cytoscape.Core) { type: 'junction', id: junction.id, parent: junction.in, - width: getConfigField('iconSize'), - height: getConfigField('iconSize'), + width: db.getConfigField('iconSize'), + height: db.getConfigField('iconSize'), } as NodeSingularData, classes: 'node-junction', }); @@ -257,7 +256,8 @@ function getAlignments( } function getRelativeConstraints( - spatialMaps: ArchitectureSpatialMap[] + spatialMaps: ArchitectureSpatialMap[], + db: ArchitectureDB ): fcose.FcoseRelativePlacementConstraint[] { const relativeConstraints: fcose.FcoseRelativePlacementConstraint[] = []; const posToStr = (pos: number[]) => `${pos[0]},${pos[1]}`; @@ -296,7 +296,7 @@ function getRelativeConstraints( [ArchitectureDirectionName[ getOppositeArchitectureDirection(dir as ArchitectureDirection) ]]: currId, - gap: 1.5 * getConfigField('iconSize'), + gap: 1.5 * db.getConfigField('iconSize'), }); } }); @@ -353,7 +353,7 @@ function layoutArchitecture( style: { 'text-valign': 'bottom', 'text-halign': 'center', - 'font-size': `${getConfigField('fontSize')}px`, + 'font-size': `${db.getConfigField('fontSize')}px`, }, }, { @@ -375,7 +375,7 @@ function layoutArchitecture( selector: '.node-group', style: { // @ts-ignore Incorrect library types - padding: `${getConfigField('padding')}px`, + padding: `${db.getConfigField('padding')}px`, }, }, ], @@ -393,14 +393,14 @@ function layoutArchitecture( renderEl.remove(); addGroups(groups, cy); - addServices(services, cy); - addJunctions(junctions, cy); + addServices(services, cy, db); + addJunctions(junctions, cy, db); addEdges(edges, cy); // Use the spatial map to create alignment arrays for fcose const alignmentConstraint = getAlignments(db, spatialMaps, groupAlignments); // Create the relative constraints for fcose by using an inverse of the spatial map and performing BFS on it - const relativePlacementConstraint = getRelativeConstraints(spatialMaps); + const relativePlacementConstraint = getRelativeConstraints(spatialMaps, db); const layout = cy.layout({ name: 'fcose', @@ -415,7 +415,9 @@ function layoutArchitecture( const { parent: parentA } = nodeData(nodeA); const { parent: parentB } = nodeData(nodeB); const elasticity = - parentA === parentB ? 1.5 * getConfigField('iconSize') : 0.5 * getConfigField('iconSize'); + parentA === parentB + ? 1.5 * db.getConfigField('iconSize') + : 0.5 * db.getConfigField('iconSize'); return elasticity; }, edgeElasticity(edge: EdgeSingular) { @@ -535,11 +537,11 @@ export const draw: DrawDefinition = async (text, id, _version, diagObj: Diagram) const cy = await layoutArchitecture(services, junctions, groups, edges, db, ds); - await drawEdges(edgesElem, cy); - await drawGroups(groupElem, cy); + await drawEdges(edgesElem, cy, db); + await drawGroups(groupElem, cy, db); positionNodes(db, cy); - setupGraphViewbox(undefined, svg, getConfigField('padding'), getConfigField('useMaxWidth')); + setupGraphViewbox(undefined, svg, db.getConfigField('padding'), db.getConfigField('useMaxWidth')); }; export const renderer = { draw }; diff --git a/packages/mermaid/src/diagrams/architecture/svgDraw.ts b/packages/mermaid/src/diagrams/architecture/svgDraw.ts index b10a451fe..f384defd8 100644 --- a/packages/mermaid/src/diagrams/architecture/svgDraw.ts +++ b/packages/mermaid/src/diagrams/architecture/svgDraw.ts @@ -1,9 +1,9 @@ -import { getIconSVG } from '../../rendering-util/icons.js'; import type cytoscape from 'cytoscape'; import { getConfig } from '../../diagram-api/diagramAPI.js'; import { createText } from '../../rendering-util/createText.js'; +import { getIconSVG } from '../../rendering-util/icons.js'; import type { D3Element } from '../../types.js'; -import { db, getConfigField } from './architectureDb.js'; +import type { ArchitectureDB } from './architectureDb.js'; import { architectureIcons } from './architectureIcons.js'; import { ArchitectureDirectionArrow, @@ -16,14 +16,17 @@ import { isArchitectureDirectionY, isArchitecturePairXY, nodeData, - type ArchitectureDB, type ArchitectureJunction, type ArchitectureService, } from './architectureTypes.js'; -export const drawEdges = async function (edgesEl: D3Element, cy: cytoscape.Core) { - const padding = getConfigField('padding'); - const iconSize = getConfigField('iconSize'); +export const drawEdges = async function ( + edgesEl: D3Element, + cy: cytoscape.Core, + db: ArchitectureDB +) { + const padding = db.getConfigField('padding'); + const iconSize = db.getConfigField('iconSize'); const halfIconSize = iconSize / 2; const arrowSize = iconSize / 6; const halfArrowSize = arrowSize / 2; @@ -183,13 +186,17 @@ export const drawEdges = async function (edgesEl: D3Element, cy: cytoscape.Core) ); }; -export const drawGroups = async function (groupsEl: D3Element, cy: cytoscape.Core) { - const padding = getConfigField('padding'); +export const drawGroups = async function ( + groupsEl: D3Element, + cy: cytoscape.Core, + db: ArchitectureDB +) { + const padding = db.getConfigField('padding'); const groupIconSize = padding * 0.75; - const fontSize = getConfigField('fontSize'); + const fontSize = db.getConfigField('fontSize'); - const iconSize = getConfigField('iconSize'); + const iconSize = db.getConfigField('iconSize'); const halfIconSize = iconSize / 2; await Promise.all( @@ -266,7 +273,7 @@ export const drawServices = async function ( ): Promise { for (const service of services) { const serviceElem = elem.append('g'); - const iconSize = getConfigField('iconSize'); + const iconSize = db.getConfigField('iconSize'); if (service.title) { const textElem = serviceElem.append('g'); @@ -350,7 +357,7 @@ export const drawJunctions = function ( ) { junctions.forEach((junction) => { const junctionElem = elem.append('g'); - const iconSize = getConfigField('iconSize'); + const iconSize = db.getConfigField('iconSize'); const bkgElem = junctionElem.append('g'); bkgElem diff --git a/packages/mermaid/src/diagrams/class/parser/class.spec.js b/packages/mermaid/src/diagrams/class/parser/class.spec.js index fe0077a29..7d4922561 100644 --- a/packages/mermaid/src/diagrams/class/parser/class.spec.js +++ b/packages/mermaid/src/diagrams/class/parser/class.spec.js @@ -15,4 +15,12 @@ describe('class diagram', function () { 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(); + }); + }); }); diff --git a/packages/mermaid/src/diagrams/class/parser/classDiagram.jison b/packages/mermaid/src/diagrams/class/parser/classDiagram.jison index 83d9bd48e..0f971c8b9 100644 --- a/packages/mermaid/src/diagrams/class/parser/classDiagram.jison +++ b/packages/mermaid/src/diagrams/class/parser/classDiagram.jison @@ -242,6 +242,7 @@ classLabel namespaceName : alphaNumToken { $$=$1; } + | classLiteralName { $$=$1; } | alphaNumToken DOT namespaceName { $$=$1+'.'+$3; } | alphaNumToken namespaceName { $$=$1+$2; } ; diff --git a/packages/mermaid/src/diagrams/flowchart/elk/detector.ts b/packages/mermaid/src/diagrams/flowchart/elk/detector.ts index 6688ffd8c..f0b2ac8ff 100644 --- a/packages/mermaid/src/diagrams/flowchart/elk/detector.ts +++ b/packages/mermaid/src/diagrams/flowchart/elk/detector.ts @@ -11,7 +11,7 @@ const detector: DiagramDetector = (txt, config = {}): boolean => { // If diagram explicitly states flowchart-elk /^\s*flowchart-elk/.test(txt) || // If a flowchart/graph diagram has their default renderer set to elk - (/^\s*flowchart|graph/.test(txt) && config?.flowchart?.defaultRenderer === 'elk') + (/^\s*(flowchart|graph)/.test(txt) && config?.flowchart?.defaultRenderer === 'elk') ) { config.layout = 'elk'; return true; diff --git a/packages/mermaid/src/diagrams/packet/db.ts b/packages/mermaid/src/diagrams/packet/db.ts index d7b550472..863bd79e1 100644 --- a/packages/mermaid/src/diagrams/packet/db.ts +++ b/packages/mermaid/src/diagrams/packet/db.ts @@ -1,6 +1,7 @@ import { getConfig as commonGetConfig } from '../../config.js'; import type { PacketDiagramConfig } from '../../config.type.js'; import DEFAULT_CONFIG from '../../defaultConfig.js'; +import type { DiagramDB } from '../../diagram-api/types.js'; import { cleanAndMerge } from '../../utils.js'; import { clear as commonClear, @@ -11,49 +12,42 @@ import { setAccTitle, setDiagramTitle, } from '../common/commonDb.js'; -import type { PacketDB, PacketData, PacketWord } from './types.js'; - -const defaultPacketData: PacketData = { - packet: [], -}; - -let data: PacketData = structuredClone(defaultPacketData); - +import type { PacketWord } from './types.js'; const DEFAULT_PACKET_CONFIG: Required = DEFAULT_CONFIG.packet; -const getConfig = (): Required => { - const config = cleanAndMerge({ - ...DEFAULT_PACKET_CONFIG, - ...commonGetConfig().packet, - }); - if (config.showBits) { - config.paddingY += 10; +export class PacketDB implements DiagramDB { + private packet: PacketWord[] = []; + + public getConfig() { + const config = cleanAndMerge({ + ...DEFAULT_PACKET_CONFIG, + ...commonGetConfig().packet, + }); + if (config.showBits) { + config.paddingY += 10; + } + return config; } - return config; -}; -const getPacket = (): PacketWord[] => data.packet; - -const pushWord = (word: PacketWord) => { - if (word.length > 0) { - data.packet.push(word); + public getPacket() { + return this.packet; } -}; -const clear = () => { - commonClear(); - data = structuredClone(defaultPacketData); -}; + public pushWord(word: PacketWord) { + if (word.length > 0) { + this.packet.push(word); + } + } -export const db: PacketDB = { - pushWord, - getPacket, - getConfig, - clear, - setAccTitle, - getAccTitle, - setDiagramTitle, - getDiagramTitle, - getAccDescription, - setAccDescription, -}; + public clear() { + commonClear(); + this.packet = []; + } + + public setAccTitle = setAccTitle; + public getAccTitle = getAccTitle; + public setDiagramTitle = setDiagramTitle; + public getDiagramTitle = getDiagramTitle; + public getAccDescription = getAccDescription; + public setAccDescription = setAccDescription; +} diff --git a/packages/mermaid/src/diagrams/packet/diagram.ts b/packages/mermaid/src/diagrams/packet/diagram.ts index a73a77c05..84a7bca8f 100644 --- a/packages/mermaid/src/diagrams/packet/diagram.ts +++ b/packages/mermaid/src/diagrams/packet/diagram.ts @@ -1,12 +1,14 @@ import type { DiagramDefinition } from '../../diagram-api/types.js'; -import { db } from './db.js'; +import { PacketDB } from './db.js'; import { parser } from './parser.js'; import { renderer } from './renderer.js'; import { styles } from './styles.js'; export const diagram: DiagramDefinition = { parser, - db, + get db() { + return new PacketDB(); + }, renderer, styles, }; diff --git a/packages/mermaid/src/diagrams/packet/packet.spec.ts b/packages/mermaid/src/diagrams/packet/packet.spec.ts index b03ffe4d1..fd7b3211a 100644 --- a/packages/mermaid/src/diagrams/packet/packet.spec.ts +++ b/packages/mermaid/src/diagrams/packet/packet.spec.ts @@ -1,24 +1,26 @@ import { it, describe, expect } from 'vitest'; -import { db } from './db.js'; +import { PacketDB } from './db.js'; import { parser } from './parser.js'; -const { clear, getPacket, getDiagramTitle, getAccTitle, getAccDescription } = db; - describe('packet diagrams', () => { + let db: PacketDB; beforeEach(() => { - clear(); + db = new PacketDB(); + if (parser.parser) { + parser.parser.yy = db; + } }); it('should handle a packet-beta definition', async () => { const str = `packet-beta`; await expect(parser.parse(str)).resolves.not.toThrow(); - expect(getPacket()).toMatchInlineSnapshot('[]'); + expect(db.getPacket()).toMatchInlineSnapshot('[]'); }); it('should handle a packet definition', async () => { const str = `packet`; await expect(parser.parse(str)).resolves.not.toThrow(); - expect(getPacket()).toMatchInlineSnapshot('[]'); + expect(db.getPacket()).toMatchInlineSnapshot('[]'); }); it('should handle diagram with data and title', async () => { @@ -29,10 +31,10 @@ describe('packet diagrams', () => { 0-10: "test" `; await expect(parser.parse(str)).resolves.not.toThrow(); - expect(getDiagramTitle()).toMatchInlineSnapshot('"Packet diagram"'); - expect(getAccTitle()).toMatchInlineSnapshot('"Packet accTitle"'); - expect(getAccDescription()).toMatchInlineSnapshot('"Packet accDescription"'); - expect(getPacket()).toMatchInlineSnapshot(` + expect(db.getDiagramTitle()).toMatchInlineSnapshot('"Packet diagram"'); + expect(db.getAccTitle()).toMatchInlineSnapshot('"Packet accTitle"'); + expect(db.getAccDescription()).toMatchInlineSnapshot('"Packet accDescription"'); + expect(db.getPacket()).toMatchInlineSnapshot(` [ [ { @@ -52,7 +54,7 @@ describe('packet diagrams', () => { 11: "single" `; await expect(parser.parse(str)).resolves.not.toThrow(); - expect(getPacket()).toMatchInlineSnapshot(` + expect(db.getPacket()).toMatchInlineSnapshot(` [ [ { @@ -78,7 +80,7 @@ describe('packet diagrams', () => { +16: "word" `; await expect(parser.parse(str)).resolves.not.toThrow(); - expect(getPacket()).toMatchInlineSnapshot(` + expect(db.getPacket()).toMatchInlineSnapshot(` [ [ { @@ -104,7 +106,7 @@ describe('packet diagrams', () => { +16: "word" `; 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" `; 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" `; await expect(parser.parse(str)).resolves.not.toThrow(); - expect(getPacket()).toMatchInlineSnapshot(` + expect(db.getPacket()).toMatchInlineSnapshot(` [ [ { diff --git a/packages/mermaid/src/diagrams/packet/parser.ts b/packages/mermaid/src/diagrams/packet/parser.ts index 1dcccd636..689d86fb3 100644 --- a/packages/mermaid/src/diagrams/packet/parser.ts +++ b/packages/mermaid/src/diagrams/packet/parser.ts @@ -3,12 +3,12 @@ import { parse } from '@mermaid-js/parser'; import type { ParserDefinition } from '../../diagram-api/types.js'; import { log } from '../../logger.js'; import { populateCommonDb } from '../common/populateCommonDb.js'; -import { db } from './db.js'; +import { PacketDB } from './db.js'; import type { PacketBlock, PacketWord } from './types.js'; const maxPacketSize = 10_000; -const populate = (ast: Packet) => { +const populate = (ast: Packet, db: PacketDB) => { populateCommonDb(ast, db); let lastBit = -1; let word: PacketWord = []; @@ -91,9 +91,17 @@ const getNextFittingBlock = ( }; export const parser: ParserDefinition = { + // @ts-expect-error - PacketDB is not assignable to DiagramDB + parser: { yy: undefined }, parse: async (input: string): Promise => { 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); - populate(ast); + populate(ast, db); }, }; diff --git a/packages/mermaid/src/docs/ecosystem/integrations-community.md b/packages/mermaid/src/docs/ecosystem/integrations-community.md index e192a0387..92a87b7ed 100644 --- a/packages/mermaid/src/docs/ecosystem/integrations-community.md +++ b/packages/mermaid/src/docs/ecosystem/integrations-community.md @@ -99,6 +99,7 @@ Blogging frameworks and platforms - [Mermaid](https://nextra.site/docs/guide/mermaid) - [WordPress](https://wordpress.org) - [MerPRess](https://wordpress.org/plugins/merpress/) + - [WP Documentation](https://wordpress.org/themes/wp-documentation/) ### CMS/ECM