diff --git a/packages/mermaid/src/Diagram.ts b/packages/mermaid/src/Diagram.ts index b56697e9de..86e7bf159c 100644 --- a/packages/mermaid/src/Diagram.ts +++ b/packages/mermaid/src/Diagram.ts @@ -1,10 +1,8 @@ import * as configApi from './config.js'; -import { log } from './logger.js'; import { getDiagram, registerDiagram } from './diagram-api/diagramAPI.js'; import { detectType, getDiagramLoader } from './diagram-api/detectType.js'; import { UnknownDiagramError } from './errors.js'; import { encodeEntities } from './utils.js'; - import type { DetailedError } from './utils.js'; import type { DiagramDefinition, DiagramMetadata } from './diagram-api/types.js'; @@ -15,51 +13,45 @@ export type ParseErrorFunction = (err: string | DetailedError | unknown, hash?: * @privateRemarks This is exported as part of the public mermaidAPI. */ export class Diagram { - type = 'graph'; - parser: DiagramDefinition['parser']; - renderer: DiagramDefinition['renderer']; - db: DiagramDefinition['db']; - private init?: DiagramDefinition['init']; - - private detectError?: UnknownDiagramError; - constructor(public text: string, public metadata: Pick = {}) { - this.text = encodeEntities(text); - this.text += '\n'; - const cnf = configApi.getConfig(); + public static async fromText(text: string, metadata: Pick = {}) { + const config = configApi.getConfig(); + const type = detectType(text, config); + text = encodeEntities(text) + '\n'; try { - this.type = detectType(text, cnf); + getDiagram(type); } catch (e) { - this.type = 'error'; - this.detectError = e as UnknownDiagramError; + const loader = getDiagramLoader(type); + if (!loader) { + throw new UnknownDiagramError(`Diagram ${type} not found.`); + } + // Diagram not available, loading it. + // new diagram will try getDiagram again and if fails then it is a valid throw + const { id, diagram } = await loader(); + registerDiagram(id, diagram); } - const diagram = getDiagram(this.type); - log.debug('Type ' + this.type); - // Setup diagram - this.db = diagram.db; - this.renderer = diagram.renderer; - this.parser = diagram.parser; - if (this.parser.parser) { + const { db, parser, renderer, init } = getDiagram(type); + if (parser.parser) { // The parser.parser.yy is only present in JISON parsers. So, we'll only set if required. - this.parser.parser.yy = this.db; + parser.parser.yy = db; } - this.init = diagram.init; - this.parse(); - } - - parse() { - if (this.detectError) { - throw this.detectError; - } - this.db.clear?.(); - const config = configApi.getConfig(); - this.init?.(config); + db.clear?.(); + init?.(config); // This block was added for legacy compatibility. Use frontmatter instead of adding more special cases. - if (this.metadata.title) { - this.db.setDiagramTitle?.(this.metadata.title); + if (metadata.title) { + db.setDiagramTitle?.(metadata.title); } - this.parser.parse(this.text); + await parser.parse(text); + return new Diagram(type, text, db, parser, renderer); } + private constructor( + public type: string, + public text: string, + public db: DiagramDefinition['db'], + public parser: DiagramDefinition['parser'], + public renderer: DiagramDefinition['renderer'] + ) {} + async render(id: string, version: string) { await this.renderer.draw(this.text, id, version, this); } @@ -72,34 +64,3 @@ export class Diagram { return this.type; } } - -/** - * Parse the text asynchronously and generate a Diagram object asynchronously. - * **Warning:** This function may be changed in the future. - * @alpha - * @param text - The mermaid diagram definition. - * @param metadata - Diagram metadata, defined in YAML. - * @returns A the Promise of a Diagram object. - * @throws {@link UnknownDiagramError} if the diagram type can not be found. - * @privateRemarks This is exported as part of the public mermaidAPI. - */ -export const getDiagramFromText = async ( - text: string, - metadata: Pick = {} -): Promise => { - const type = detectType(text, configApi.getConfig()); - try { - // Trying to find the diagram - getDiagram(type); - } catch (error) { - const loader = getDiagramLoader(type); - if (!loader) { - throw new UnknownDiagramError(`Diagram ${type} not found.`); - } - // Diagram not available, loading it. - // new diagram will try getDiagram again and if fails then it is a valid throw - const { id, diagram } = await loader(); - registerDiagram(id, diagram); - } - return new Diagram(text, metadata); -}; diff --git a/packages/mermaid/src/diagram-api/diagramAPI.spec.ts b/packages/mermaid/src/diagram-api/diagramAPI.spec.ts index 3b6bce683f..fd0e458425 100644 --- a/packages/mermaid/src/diagram-api/diagramAPI.spec.ts +++ b/packages/mermaid/src/diagram-api/diagramAPI.spec.ts @@ -2,12 +2,12 @@ import { detectType } from './detectType.js'; import { getDiagram, registerDiagram } from './diagramAPI.js'; import { addDiagrams } from './diagram-orchestration.js'; import type { DiagramDetector } from './types.js'; -import { getDiagramFromText } from '../Diagram.js'; +import { Diagram } from '../Diagram.js'; import { it, describe, expect, beforeAll } from 'vitest'; addDiagrams(); beforeAll(async () => { - await getDiagramFromText('sequenceDiagram'); + await Diagram.fromText('sequenceDiagram'); }); describe('DiagramAPI', () => { diff --git a/packages/mermaid/src/diagram-api/types.ts b/packages/mermaid/src/diagram-api/types.ts index 88957b5fb8..6ab82bd0dc 100644 --- a/packages/mermaid/src/diagram-api/types.ts +++ b/packages/mermaid/src/diagram-api/types.ts @@ -121,7 +121,7 @@ export type DrawDefinition = ( ) => void | Promise; export interface ParserDefinition { - parse: (text: string) => void; + parse: (text: string) => void | Promise; parser?: { yy: DiagramDB }; } diff --git a/packages/mermaid/src/diagram.spec.ts b/packages/mermaid/src/diagram.spec.ts index c73fb0a3b0..46054ed6de 100644 --- a/packages/mermaid/src/diagram.spec.ts +++ b/packages/mermaid/src/diagram.spec.ts @@ -1,5 +1,5 @@ import { describe, test, expect } from 'vitest'; -import { Diagram, getDiagramFromText } from './Diagram.js'; +import { Diagram } from './Diagram.js'; import { addDetector } from './diagram-api/detectType.js'; import { addDiagrams } from './diagram-api/diagram-orchestration.js'; import type { DiagramLoader } from './diagram-api/types.js'; @@ -30,10 +30,10 @@ const getDummyDiagram = (id: string, title?: string): Awaited { test('should detect inbuilt diagrams', async () => { - const graph = (await getDiagramFromText('graph TD; A-->B')) as Diagram; + const graph = (await Diagram.fromText('graph TD; A-->B')) as Diagram; expect(graph).toBeInstanceOf(Diagram); expect(graph.type).toBe('flowchart-v2'); - const sequence = (await getDiagramFromText( + const sequence = (await Diagram.fromText( 'sequenceDiagram; Alice->>+John: Hello John, how are you?' )) as Diagram; expect(sequence).toBeInstanceOf(Diagram); @@ -46,7 +46,7 @@ describe('diagram detection', () => { (str) => str.startsWith('loki'), () => Promise.resolve(getDummyDiagram('loki')) ); - const diagram = await getDiagramFromText('loki TD; A-->B'); + const diagram = await Diagram.fromText('loki TD; A-->B'); expect(diagram).toBeInstanceOf(Diagram); expect(diagram.type).toBe('loki'); }); @@ -58,19 +58,19 @@ describe('diagram detection', () => { (str) => str.startsWith('flowchart-elk'), () => Promise.resolve(getDummyDiagram('flowchart-elk', title)) ); - const diagram = await getDiagramFromText('flowchart-elk TD; A-->B'); + const diagram = await Diagram.fromText('flowchart-elk TD; A-->B'); expect(diagram).toBeInstanceOf(Diagram); expect(diagram.db.getDiagramTitle?.()).toBe(title); }); test('should throw the right error for incorrect diagram', async () => { - await expect(getDiagramFromText('graph TD; A-->')).rejects.toThrowErrorMatchingInlineSnapshot(` + await expect(Diagram.fromText('graph TD; A-->')).rejects.toThrowErrorMatchingInlineSnapshot(` "Parse error on line 2: graph TD; A--> --------------^ Expecting 'AMP', 'COLON', 'PIPE', 'TESTSTR', 'DOWN', 'DEFAULT', 'NUM', 'COMMA', 'NODE_STRING', 'BRKT', 'MINUS', 'MULT', 'UNICODE_TEXT', got 'EOF'" `); - await expect(getDiagramFromText('sequenceDiagram; A-->B')).rejects + await expect(Diagram.fromText('sequenceDiagram; A-->B')).rejects .toThrowErrorMatchingInlineSnapshot(` "Parse error on line 1: ...quenceDiagram; A-->B @@ -80,13 +80,13 @@ Expecting 'TXT', got 'NEWLINE'" }); test('should throw the right error for unregistered diagrams', async () => { - await expect(getDiagramFromText('thor TD; A-->B')).rejects.toThrowErrorMatchingInlineSnapshot( + await expect(Diagram.fromText('thor TD; A-->B')).rejects.toThrowErrorMatchingInlineSnapshot( '"No diagram type detected matching given configuration for text: thor TD; A-->B"' ); }); test('should consider entity codes when present in diagram defination', async () => { - const diagram = await getDiagramFromText(`sequenceDiagram + const diagram = await Diagram.fromText(`sequenceDiagram A->>B: I #9829; you! B->>A: I #9829; you #infin; times more!`); // @ts-ignore: we need to add types for sequenceDb which will be done in separate PR diff --git a/packages/mermaid/src/diagrams/info/info.spec.ts b/packages/mermaid/src/diagrams/info/info.spec.ts index b7adf9f2ed..6e139ab78c 100644 --- a/packages/mermaid/src/diagrams/info/info.spec.ts +++ b/packages/mermaid/src/diagrams/info/info.spec.ts @@ -1,31 +1,27 @@ import { parser } from './infoParser.js'; describe('info', () => { - it('should handle an info definition', () => { + it('should handle an info definition', async () => { const str = `info`; - expect(() => { - parser.parse(str); - }).not.toThrow(); + await expect(parser.parse(str)).resolves.not.toThrow(); }); - it('should handle an info definition with showInfo', () => { + it('should handle an info definition with showInfo', async () => { const str = `info showInfo`; - expect(() => { - parser.parse(str); - }).not.toThrow(); + await expect(parser.parse(str)).resolves.not.toThrow(); }); - it('should throw because of unsupported info grammar', () => { + it('should throw because of unsupported info grammar', async () => { const str = `info unsupported`; - expect(() => { - parser.parse(str); - }).toThrow('Parsing failed: unexpected character: ->u<- at offset: 5, skipped 11 characters.'); + await expect(parser.parse(str)).rejects.toThrow( + 'Parsing failed: unexpected character: ->u<- at offset: 5, skipped 11 characters.' + ); }); - it('should throw because of unsupported info grammar', () => { + it('should throw because of unsupported info grammar', async () => { const str = `info unsupported`; - expect(() => { - parser.parse(str); - }).toThrow('Parsing failed: unexpected character: ->u<- at offset: 5, skipped 11 characters.'); + await expect(parser.parse(str)).rejects.toThrow( + 'Parsing failed: unexpected character: ->u<- at offset: 5, skipped 11 characters.' + ); }); }); diff --git a/packages/mermaid/src/diagrams/info/infoParser.ts b/packages/mermaid/src/diagrams/info/infoParser.ts index 19c13a0460..5fd54258ab 100644 --- a/packages/mermaid/src/diagrams/info/infoParser.ts +++ b/packages/mermaid/src/diagrams/info/infoParser.ts @@ -1,12 +1,11 @@ import type { Info } 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'; export const parser: ParserDefinition = { - parse: (input: string): void => { - const ast: Info = parse('info', input); + parse: async (input: string): Promise => { + const ast: Info = await parse('info', input); log.debug(ast); }, }; diff --git a/packages/mermaid/src/diagrams/packet/packet.spec.ts b/packages/mermaid/src/diagrams/packet/packet.spec.ts index 87432f4891..b053ea6275 100644 --- a/packages/mermaid/src/diagrams/packet/packet.spec.ts +++ b/packages/mermaid/src/diagrams/packet/packet.spec.ts @@ -1,3 +1,4 @@ +import { it, describe, expect } from 'vitest'; import { db } from './db.js'; import { parser } from './parser.js'; @@ -8,24 +9,20 @@ describe('packet diagrams', () => { clear(); }); - it('should handle a packet-beta definition', () => { + it('should handle a packet-beta definition', async () => { const str = `packet-beta`; - expect(() => { - parser.parse(str); - }).not.toThrow(); + await expect(parser.parse(str)).resolves.not.toThrow(); expect(getPacket()).toMatchInlineSnapshot('[]'); }); - it('should handle diagram with data and title', () => { + it('should handle diagram with data and title', async () => { const str = `packet-beta title Packet diagram accTitle: Packet accTitle accDescr: Packet accDescription 0-10: "test" `; - expect(() => { - parser.parse(str); - }).not.toThrow(); + await expect(parser.parse(str)).resolves.not.toThrow(); expect(getDiagramTitle()).toMatchInlineSnapshot('"Packet diagram"'); expect(getAccTitle()).toMatchInlineSnapshot('"Packet accTitle"'); expect(getAccDescription()).toMatchInlineSnapshot('"Packet accDescription"'); @@ -42,14 +39,12 @@ describe('packet diagrams', () => { `); }); - it('should handle single bits', () => { + it('should handle single bits', async () => { const str = `packet-beta 0-10: "test" 11: "single" `; - expect(() => { - parser.parse(str); - }).not.toThrow(); + await expect(parser.parse(str)).resolves.not.toThrow(); expect(getPacket()).toMatchInlineSnapshot(` [ [ @@ -68,14 +63,12 @@ describe('packet diagrams', () => { `); }); - it('should split into multiple rows', () => { + it('should split into multiple rows', async () => { const str = `packet-beta 0-10: "test" 11-90: "multiple" `; - expect(() => { - parser.parse(str); - }).not.toThrow(); + await expect(parser.parse(str)).resolves.not.toThrow(); expect(getPacket()).toMatchInlineSnapshot(` [ [ @@ -108,14 +101,12 @@ describe('packet diagrams', () => { `); }); - it('should split into multiple rows when cut at exact length', () => { + it('should split into multiple rows when cut at exact length', async () => { const str = `packet-beta 0-16: "test" 17-63: "multiple" `; - expect(() => { - parser.parse(str); - }).not.toThrow(); + await expect(parser.parse(str)).resolves.not.toThrow(); expect(getPacket()).toMatchInlineSnapshot(` [ [ @@ -141,51 +132,43 @@ describe('packet diagrams', () => { `); }); - it('should throw error if numbers are not continuous', () => { + it('should throw error if numbers are not continuous', async () => { const str = `packet-beta 0-16: "test" 18-20: "error" `; - expect(() => { - parser.parse(str); - }).toThrowErrorMatchingInlineSnapshot( + await expect(parser.parse(str)).rejects.toThrowErrorMatchingInlineSnapshot( '"Packet block 18 - 20 is not contiguous. It should start from 17."' ); }); - it('should throw error if numbers are not continuous for single packets', () => { + it('should throw error if numbers are not continuous for single packets', async () => { const str = `packet-beta 0-16: "test" 18: "error" `; - expect(() => { - parser.parse(str); - }).toThrowErrorMatchingInlineSnapshot( + await expect(parser.parse(str)).rejects.toThrowErrorMatchingInlineSnapshot( '"Packet block 18 - 18 is not contiguous. It should start from 17."' ); }); - it('should throw error if numbers are not continuous for single packets - 2', () => { + it('should throw error if numbers are not continuous for single packets - 2', async () => { const str = `packet-beta 0-16: "test" 17: "good" 19: "error" `; - expect(() => { - parser.parse(str); - }).toThrowErrorMatchingInlineSnapshot( + await expect(parser.parse(str)).rejects.toThrowErrorMatchingInlineSnapshot( '"Packet block 19 - 19 is not contiguous. It should start from 18."' ); }); - it('should throw error if end is less than start', () => { + it('should throw error if end is less than start', async () => { const str = `packet-beta 0-16: "test" 25-20: "error" `; - expect(() => { - parser.parse(str); - }).toThrowErrorMatchingInlineSnapshot( + await expect(parser.parse(str)).rejects.toThrowErrorMatchingInlineSnapshot( '"Packet block 25 - 20 is invalid. End must be greater than start."' ); }); diff --git a/packages/mermaid/src/diagrams/packet/parser.ts b/packages/mermaid/src/diagrams/packet/parser.ts index d7cc1f06fb..06d180dfd6 100644 --- a/packages/mermaid/src/diagrams/packet/parser.ts +++ b/packages/mermaid/src/diagrams/packet/parser.ts @@ -77,8 +77,8 @@ const getNextFittingBlock = ( }; export const parser: ParserDefinition = { - parse: (input: string): void => { - const ast: Packet = parse('packet', input); + parse: async (input: string): Promise => { + const ast: Packet = await parse('packet', input); log.debug(ast); populate(ast); }, diff --git a/packages/mermaid/src/diagrams/sequence/sequenceDiagram.spec.js b/packages/mermaid/src/diagrams/sequence/sequenceDiagram.spec.js index 8a7e2281cb..5ec99f7eaa 100644 --- a/packages/mermaid/src/diagrams/sequence/sequenceDiagram.spec.js +++ b/packages/mermaid/src/diagrams/sequence/sequenceDiagram.spec.js @@ -1,12 +1,12 @@ import { vi } from 'vitest'; import { setSiteConfig } from '../../diagram-api/diagramAPI.js'; import mermaidAPI from '../../mermaidAPI.js'; -import { Diagram, getDiagramFromText } from '../../Diagram.js'; +import { Diagram } from '../../Diagram.js'; import { addDiagrams } from '../../diagram-api/diagram-orchestration.js'; beforeAll(async () => { // Is required to load the sequence diagram - await getDiagramFromText('sequenceDiagram'); + await Diagram.fromText('sequenceDiagram'); }); /** @@ -95,8 +95,8 @@ function addConf(conf, key, value) { let diagram; describe('more than one sequence diagram', () => { - it('should not have duplicated messages', () => { - const diagram1 = new Diagram(` + it('should not have duplicated messages', async () => { + const diagram1 = await Diagram.fromText(` sequenceDiagram Alice->Bob:Hello Bob, how are you? Bob-->Alice: I am good thanks!`); @@ -120,7 +120,7 @@ describe('more than one sequence diagram', () => { }, ] `); - const diagram2 = new Diagram(` + const diagram2 = await Diagram.fromText(` sequenceDiagram Alice->Bob:Hello Bob, how are you? Bob-->Alice: I am good thanks!`); @@ -147,7 +147,7 @@ describe('more than one sequence diagram', () => { `); // Add John actor - const diagram3 = new Diagram(` + const diagram3 = await Diagram.fromText(` sequenceDiagram Alice->John:Hello John, how are you? John-->Alice: I am good thanks!`); @@ -176,8 +176,8 @@ describe('more than one sequence diagram', () => { }); describe('when parsing a sequenceDiagram', function () { - beforeEach(function () { - diagram = new Diagram(` + beforeEach(async function () { + diagram = await Diagram.fromText(` sequenceDiagram Alice->Bob:Hello Bob, how are you? Note right of Bob: Bob thinks @@ -1613,7 +1613,7 @@ describe('when rendering a sequenceDiagram APA', function () { setSiteConfig({ logLevel: 5, sequence: conf }); }); let conf; - beforeEach(function () { + beforeEach(async function () { mermaidAPI.reset(); // }); @@ -1632,7 +1632,7 @@ describe('when rendering a sequenceDiagram APA', function () { mirrorActors: false, }; setSiteConfig({ logLevel: 5, sequence: conf }); - diagram = new Diagram(` + diagram = await Diagram.fromText(` sequenceDiagram Alice->Bob:Hello Bob, how are you? Note right of Bob: Bob thinks diff --git a/packages/mermaid/src/mermaidAPI.ts b/packages/mermaid/src/mermaidAPI.ts index 166bc25ad8..0b37764ae2 100644 --- a/packages/mermaid/src/mermaidAPI.ts +++ b/packages/mermaid/src/mermaidAPI.ts @@ -17,7 +17,7 @@ import { compile, serialize, stringify } from 'stylis'; import { version } from '../package.json'; import * as configApi from './config.js'; import { addDiagrams } from './diagram-api/diagram-orchestration.js'; -import { Diagram, getDiagramFromText as getDiagramFromTextInternal } from './Diagram.js'; +import { Diagram } from './Diagram.js'; import errorRenderer from './diagrams/error/errorRenderer.js'; import { attachFunctions } from './interactionDb.js'; import { log, setLogLevel } from './logger.js'; @@ -422,9 +422,9 @@ const render = async function ( let parseEncounteredException; try { - diag = await getDiagramFromText(text, { title: processed.title }); + diag = await Diagram.fromText(text, { title: processed.title }); } catch (error) { - diag = new Diagram('error'); + diag = await Diagram.fromText('error'); parseEncounteredException = error; } @@ -536,7 +536,7 @@ function initialize(options: MermaidConfig = {}) { const getDiagramFromText = (text: string, metadata: Pick = {}) => { const { code } = preprocessDiagram(text); - return getDiagramFromTextInternal(code, metadata); + return Diagram.fromText(code, metadata); }; /** diff --git a/packages/parser/src/parse.ts b/packages/parser/src/parse.ts index eba118e417..855fc272bc 100644 --- a/packages/parser/src/parse.ts +++ b/packages/parser/src/parse.ts @@ -1,35 +1,34 @@ import type { LangiumParser, ParseResult } from 'langium'; import type { Info, Packet } from './index.js'; -import { createInfoServices, createPacketServices } from './language/index.js'; export type DiagramAST = Info | Packet; const parsers: Record = {}; const initializers = { - info: () => { - // Will have to make parse async to use this. Can try later... - // const { createInfoServices } = await import('./language/info/index.js'); + info: async () => { + const { createInfoServices } = await import('./language/info/index.js'); const parser = createInfoServices().Info.parser.LangiumParser; parsers['info'] = parser; }, - packet: () => { + packet: async () => { + const { createPacketServices } = await import('./language/packet/index.js'); const parser = createPacketServices().Packet.parser.LangiumParser; parsers['packet'] = parser; }, } as const; -export function parse(diagramType: 'info', text: string): Info; -export function parse(diagramType: 'packet', text: string): Packet; -export function parse( +export async function parse(diagramType: 'info', text: string): Promise; +export async function parse(diagramType: 'packet', text: string): Promise; +export async function parse( diagramType: keyof typeof initializers, text: string -): T { +): Promise { const initializer = initializers[diagramType]; if (!initializer) { throw new Error(`Unknown diagram type: ${diagramType}`); } if (!parsers[diagramType]) { - initializer(); + await initializer(); } const parser: LangiumParser = parsers[diagramType]; const result: ParseResult = parser.parse(text);