diff --git a/packages/mermaid/src/diagram-api/detectType.ts b/packages/mermaid/src/diagram-api/detectType.ts index 3c9237e5b..3cf61af39 100644 --- a/packages/mermaid/src/diagram-api/detectType.ts +++ b/packages/mermaid/src/diagram-api/detectType.ts @@ -7,6 +7,7 @@ import type { ExternalDiagramDefinition, } from './types'; import { frontMatterRegex } from './frontmatter'; +import { getDiagram, registerDiagram } from './diagramAPI'; import { UnknownDiagramError } from '../errors'; const directive = /%{2}{\s*(?:(\w+)\s*:|(\w+))\s*(?:(\w+)|((?:(?!}%{2}).|\r?\n)*))?\s*(?:}%{2})?/gi; @@ -54,6 +55,39 @@ export const registerLazyLoadedDiagrams = (...diagrams: ExternalDiagramDefinitio } }; +export const loadRegisteredDiagrams = async () => { + log.debug(`Loading registered diagrams`); + // Load all lazy loaded diagrams in parallel + const results = await Promise.allSettled( + Object.entries(detectors).map(async ([key, { detector, loader }]) => { + if (loader) { + try { + getDiagram(key); + } catch (error) { + try { + // Register diagram if it is not already registered + const { diagram, id } = await loader(); + registerDiagram(id, diagram, detector); + } catch (err) { + // Remove failed diagram from detectors + log.error(`Failed to load external diagram with key ${key}. Remozing from detectors.`); + delete detectors[key]; + throw err; + } + } + } + }) + ); + const failed = results.filter((result) => result.status === 'rejected'); + if (failed.length > 0) { + log.error(`Failed to load ${failed.length} external diagrams`); + for (const res of failed) { + log.error(res); + } + throw new Error(`Failed to load ${failed.length} external diagrams`); + } +}; + export const addDetector = (key: string, detector: DiagramDetector, loader?: DiagramLoader) => { if (detectors[key]) { log.error(`Detector with key ${key} already exists`); diff --git a/packages/mermaid/src/diagram-api/diagram-orchestration.ts b/packages/mermaid/src/diagram-api/diagram-orchestration.ts index 76b20ce5a..d06ce846a 100644 --- a/packages/mermaid/src/diagram-api/diagram-orchestration.ts +++ b/packages/mermaid/src/diagram-api/diagram-orchestration.ts @@ -18,6 +18,7 @@ import flowchartElk from '../diagrams/flowchart/elk/detector'; import timeline from '../diagrams/timeline/detector'; import mindmap from '../diagrams/mindmap/detector'; import { registerLazyLoadedDiagrams } from './detectType'; +import { registerDiagram } from './diagramAPI'; let hasLoadedDiagrams = false; export const addDiagrams = () => { @@ -27,6 +28,33 @@ export const addDiagrams = () => { // This is added here to avoid race-conditions. // We could optimize the loading logic somehow. hasLoadedDiagrams = true; + registerDiagram( + '---', + // --- diagram type may appear if YAML front-matter is not parsed correctly + { + db: { + clear: () => { + // Quite ok, clear needs to be there for --- to work as a regular diagram + }, + }, + styles: {}, // should never be used + renderer: {}, // should never be used + parser: { + parser: { yy: {} }, + parse: () => { + throw new Error( + 'Diagrams beginning with --- are not valid. ' + + 'If you were trying to use a YAML front-matter, please ensure that ' + + "you've correctly opened and closed the YAML front-matter with unindented `---` blocks" + ); + }, + }, + init: () => null, // no op + }, + (text) => { + return text.toLowerCase().trimStart().startsWith('---'); + } + ); registerLazyLoadedDiagrams( error, c4, diff --git a/packages/mermaid/src/diagram-api/diagramAPI.spec.ts b/packages/mermaid/src/diagram-api/diagramAPI.spec.ts index ea546fbb6..9e04c861f 100644 --- a/packages/mermaid/src/diagram-api/diagramAPI.spec.ts +++ b/packages/mermaid/src/diagram-api/diagramAPI.spec.ts @@ -2,8 +2,12 @@ import { detectType } from './detectType'; import { getDiagram, registerDiagram } from './diagramAPI'; import { addDiagrams } from './diagram-orchestration'; import { DiagramDetector } from './types'; +import { getDiagramFromText } from '../Diagram'; addDiagrams(); +beforeAll(async () => { + await getDiagramFromText('sequenceDiagram'); +}); describe('DiagramAPI', () => { it('should return default diagrams', () => { diff --git a/packages/mermaid/src/diagram-api/diagramAPI.ts b/packages/mermaid/src/diagram-api/diagramAPI.ts index 105c60e60..c5841b96b 100644 --- a/packages/mermaid/src/diagram-api/diagramAPI.ts +++ b/packages/mermaid/src/diagram-api/diagramAPI.ts @@ -4,7 +4,7 @@ import { getConfig as _getConfig } from '../config'; import { sanitizeText as _sanitizeText } from '../diagrams/common/common'; import { setupGraphViewbox as _setupGraphViewbox } from '../setupGraphViewbox'; import { addStylesForDiagram } from '../styles'; -import { DiagramDefinition, DiagramDetector, ExternalDiagramDefinition } from './types'; +import { DiagramDefinition, DiagramDetector } from './types'; import * as _commonDb from '../commonDb'; import { parseDirective as _parseDirective } from '../directiveUtils'; @@ -77,27 +77,3 @@ export class DiagramNotFoundError extends Error { super(`Diagram ${message} not found.`); } } - -/** - * This is an internal function and should not be made public, as it will likely change. - * @internal - * @param diagrams - Array of {@link ExternalDiagramDefinition}. - */ -export const loadExternalDiagrams = async (...diagrams: ExternalDiagramDefinition[]) => { - log.debug(`Loading ${diagrams.length} external diagrams`); - // Load all lazy loaded diagrams in parallel - const results = await Promise.allSettled( - diagrams.map(async ({ id, detector, loader }) => { - const { diagram } = await loader(); - registerDiagram(id, diagram, detector); - }) - ); - const failed = results.filter((result) => result.status === 'rejected'); - if (failed.length > 0) { - log.error(`Failed to load ${failed.length} external diagrams`); - for (const res of failed) { - log.error(res); - } - throw new Error(`Failed to load ${failed.length} external diagrams`); - } -}; diff --git a/packages/mermaid/src/diagrams/flowchart/flowRenderer.addEdges.spec.js b/packages/mermaid/src/diagrams/flowchart/flowRenderer.addEdges.spec.js index 01b6163cb..7744053f0 100644 --- a/packages/mermaid/src/diagrams/flowchart/flowRenderer.addEdges.spec.js +++ b/packages/mermaid/src/diagrams/flowchart/flowRenderer.addEdges.spec.js @@ -1,21 +1,24 @@ import flowDb from './flowDb'; -import flowParser from './parser/flow'; +import { parser } from './parser/flow'; import flowRenderer from './flowRenderer'; -import { Diagram } from '../../Diagram'; import { addDiagrams } from '../../diagram-api/diagram-orchestration'; + +const diag = { + db: flowDb, +}; addDiagrams(); describe('when using mermaid and ', function () { describe('when calling addEdges ', function () { beforeEach(function () { - flowParser.parser.yy = flowDb; + parser.yy = flowDb; flowDb.clear(); flowDb.setGen('gen-2'); }); - it('should handle edges with text', function () { - const diag = new Diagram('graph TD;A-->|text ex|B;'); - diag.db.getVertices(); - const edges = diag.db.getEdges(); + it('should handle edges with text', () => { + parser.parse('graph TD;A-->|text ex|B;'); + flowDb.getVertices(); + const edges = flowDb.getEdges(); const mockG = { setEdge: function (start, end, options) { @@ -29,10 +32,10 @@ describe('when using mermaid and ', function () { flowRenderer.addEdges(edges, mockG, diag); }); - it('should handle edges without text', function () { - const diag = new Diagram('graph TD;A-->B;'); - diag.db.getVertices(); - const edges = diag.db.getEdges(); + it('should handle edges without text', async function () { + parser.parse('graph TD;A-->B;'); + flowDb.getVertices(); + const edges = flowDb.getEdges(); const mockG = { setEdge: function (start, end, options) { @@ -45,10 +48,10 @@ describe('when using mermaid and ', function () { flowRenderer.addEdges(edges, mockG, diag); }); - it('should handle open-ended edges', function () { - const diag = new Diagram('graph TD;A---B;'); - diag.db.getVertices(); - const edges = diag.db.getEdges(); + it('should handle open-ended edges', () => { + parser.parse('graph TD;A---B;'); + flowDb.getVertices(); + const edges = flowDb.getEdges(); const mockG = { setEdge: function (start, end, options) { @@ -61,10 +64,10 @@ describe('when using mermaid and ', function () { flowRenderer.addEdges(edges, mockG, diag); }); - it('should handle edges with styles defined', function () { - const diag = new Diagram('graph TD;A---B; linkStyle 0 stroke:val1,stroke-width:val2;'); - diag.db.getVertices(); - const edges = diag.db.getEdges(); + it('should handle edges with styles defined', () => { + parser.parse('graph TD;A---B; linkStyle 0 stroke:val1,stroke-width:val2;'); + flowDb.getVertices(); + const edges = flowDb.getEdges(); const mockG = { setEdge: function (start, end, options) { @@ -77,10 +80,10 @@ describe('when using mermaid and ', function () { flowRenderer.addEdges(edges, mockG, diag); }); - it('should handle edges with interpolation defined', function () { - const diag = new Diagram('graph TD;A---B; linkStyle 0 interpolate basis'); - diag.db.getVertices(); - const edges = diag.db.getEdges(); + it('should handle edges with interpolation defined', () => { + parser.parse('graph TD;A---B; linkStyle 0 interpolate basis'); + flowDb.getVertices(); + const edges = flowDb.getEdges(); const mockG = { setEdge: function (start, end, options) { @@ -93,12 +96,10 @@ describe('when using mermaid and ', function () { flowRenderer.addEdges(edges, mockG, diag); }); - it('should handle edges with text and styles defined', function () { - const diag = new Diagram( - 'graph TD;A---|the text|B; linkStyle 0 stroke:val1,stroke-width:val2;' - ); - diag.db.getVertices(); - const edges = diag.db.getEdges(); + it('should handle edges with text and styles defined', () => { + parser.parse('graph TD;A---|the text|B; linkStyle 0 stroke:val1,stroke-width:val2;'); + flowDb.getVertices(); + const edges = flowDb.getEdges(); const mockG = { setEdge: function (start, end, options) { @@ -113,10 +114,10 @@ describe('when using mermaid and ', function () { flowRenderer.addEdges(edges, mockG, diag); }); - it('should set fill to "none" by default when handling edges', function () { - const diag = new Diagram('graph TD;A---B; linkStyle 0 stroke:val1,stroke-width:val2;'); - diag.db.getVertices(); - const edges = diag.db.getEdges(); + it('should set fill to "none" by default when handling edges', () => { + parser.parse('graph TD;A---B; linkStyle 0 stroke:val1,stroke-width:val2;'); + flowDb.getVertices(); + const edges = flowDb.getEdges(); const mockG = { setEdge: function (start, end, options) { @@ -130,12 +131,10 @@ describe('when using mermaid and ', function () { flowRenderer.addEdges(edges, mockG, diag); }); - it('should not set fill to none if fill is set in linkStyle', function () { - const diag = new Diagram( - 'graph TD;A---B; linkStyle 0 stroke:val1,stroke-width:val2,fill:blue;' - ); - diag.db.getVertices(); - const edges = diag.db.getEdges(); + it('should not set fill to none if fill is set in linkStyle', () => { + parser.parse('graph TD;A---B; linkStyle 0 stroke:val1,stroke-width:val2,fill:blue;'); + flowDb.getVertices(); + const edges = flowDb.getEdges(); const mockG = { setEdge: function (start, end, options) { expect(start).toContain('flowchart-A-'); diff --git a/packages/mermaid/src/diagrams/sequence/sequenceDiagram.spec.js b/packages/mermaid/src/diagrams/sequence/sequenceDiagram.spec.js index 72daca932..08f6abee1 100644 --- a/packages/mermaid/src/diagrams/sequence/sequenceDiagram.spec.js +++ b/packages/mermaid/src/diagrams/sequence/sequenceDiagram.spec.js @@ -2,9 +2,14 @@ import { vi } from 'vitest'; import * as configApi from '../../config'; import mermaidAPI from '../../mermaidAPI'; -import { Diagram } from '../../Diagram'; +import { Diagram, getDiagramFromText } from '../../Diagram'; import { addDiagrams } from '../../diagram-api/diagram-orchestration'; +beforeAll(async () => { + // Is required to load the sequence diagram + await getDiagramFromText('sequenceDiagram'); +}); + /** * Sequence diagrams require their own very special version of a mocked d3 module * diagrams/sequence/svgDraw uses statements like this with d3 nodes: (note the [0][0]) @@ -183,7 +188,7 @@ Alice->Bob:Hello Bob, how are you? Note right of Bob: Bob thinks Bob-->Alice: I am good thanks!`; - await mermaidAPI.parse(str, diagram); + await mermaidAPI.parse(str); const actors = diagram.db.getActors(); expect(actors.Alice.description).toBe('Alice'); actors.Bob.description = 'Bob'; diff --git a/packages/mermaid/src/mermaid.spec.ts b/packages/mermaid/src/mermaid.spec.ts index 75cafcdf3..58d39c348 100644 --- a/packages/mermaid/src/mermaid.spec.ts +++ b/packages/mermaid/src/mermaid.spec.ts @@ -1,6 +1,11 @@ import mermaid from './mermaid'; import { mermaidAPI } from './mermaidAPI'; import './diagram-api/diagram-orchestration'; +import { addDiagrams } from './diagram-api/diagram-orchestration'; + +beforeAll(async () => { + addDiagrams(); +}); const spyOn = vi.spyOn; vi.mock('./mermaidAPI'); @@ -66,9 +71,9 @@ describe('when using mermaid and ', () => { mermaid.registerExternalDiagrams( [ { - id: 'dummy', - detector: (text) => /dummy/.test(text), - loader: () => Promise.reject('error'), + id: 'dummyError', + detector: (text) => /dummyError/.test(text), + loader: () => Promise.reject('dummyError'), }, ], { lazyLoad: false } diff --git a/packages/mermaid/src/mermaid.ts b/packages/mermaid/src/mermaid.ts index 29d25c016..7238fcb43 100644 --- a/packages/mermaid/src/mermaid.ts +++ b/packages/mermaid/src/mermaid.ts @@ -7,11 +7,10 @@ import { MermaidConfig } from './config.type'; import { log } from './logger'; import utils from './utils'; import { mermaidAPI, ParseOptions, RenderResult } from './mermaidAPI'; -import { registerLazyLoadedDiagrams } from './diagram-api/detectType'; +import { registerLazyLoadedDiagrams, loadRegisteredDiagrams } from './diagram-api/detectType'; import type { ParseErrorFunction } from './Diagram'; import { isDetailedError } from './utils'; import type { DetailedError } from './utils'; -import { registerDiagram } from './diagram-api/diagramAPI'; import { ExternalDiagramDefinition } from './diagram-api/types'; export type { @@ -64,30 +63,6 @@ const handleError = (error: unknown, errors: DetailedError[], parseError?: Parse } }; -/** - * This is an internal function and should not be made public, as it will likely change. - * @internal - * @param diagrams - Array of {@link ExternalDiagramDefinition}. - */ -const loadExternalDiagrams = async (...diagrams: ExternalDiagramDefinition[]) => { - log.debug(`Loading ${diagrams.length} external diagrams`); - // Load all lazy loaded diagrams in parallel - const results = await Promise.allSettled( - diagrams.map(async ({ id, detector, loader }) => { - const { diagram } = await loader(); - registerDiagram(id, diagram, detector); - }) - ); - const failed = results.filter((result) => result.status === 'rejected'); - if (failed.length > 0) { - log.error(`Failed to load ${failed.length} external diagrams`); - for (const res of failed) { - log.error(res); - } - throw new Error(`Failed to load ${failed.length} external diagrams`); - } -}; - /** * ## run * @@ -251,7 +226,7 @@ const init = async function ( /** * Used to register external diagram types. * @param diagrams - Array of {@link ExternalDiagramDefinition}. - * @param opts - If opts.lazyLoad is true, the diagram will be loaded on demand. + * @param opts - If opts.lazyLoad is false, the diagrams will be loaded immediately. */ const registerExternalDiagrams = async ( diagrams: ExternalDiagramDefinition[], @@ -261,10 +236,9 @@ const registerExternalDiagrams = async ( lazyLoad?: boolean; } = {} ) => { - if (lazyLoad) { - registerLazyLoadedDiagrams(...diagrams); - } else { - await loadExternalDiagrams(...diagrams); + registerLazyLoadedDiagrams(...diagrams); + if (lazyLoad === false) { + await loadRegisteredDiagrams(); } };