Skip to content

Commit

Permalink
Merge pull request #5233 from mermaid-js/AsyncParse
Browse files Browse the repository at this point in the history
refactor: Support async parsers [internal]
  • Loading branch information
sidharthv96 authored Jan 27, 2024
2 parents b7c72cb + 442da6c commit 969e2c1
Show file tree
Hide file tree
Showing 11 changed files with 101 additions and 163 deletions.
99 changes: 30 additions & 69 deletions packages/mermaid/src/Diagram.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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<DiagramMetadata, 'title'> = {}) {
this.text = encodeEntities(text);
this.text += '\n';
const cnf = configApi.getConfig();
public static async fromText(text: string, metadata: Pick<DiagramMetadata, 'title'> = {}) {
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);
}
Expand All @@ -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<DiagramMetadata, 'title'> = {}
): Promise<Diagram> => {
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);
};
4 changes: 2 additions & 2 deletions packages/mermaid/src/diagram-api/diagramAPI.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/mermaid/src/diagram-api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ export type DrawDefinition = (
) => void | Promise<void>;

export interface ParserDefinition {
parse: (text: string) => void;
parse: (text: string) => void | Promise<void>;
parser?: { yy: DiagramDB };
}

Expand Down
18 changes: 9 additions & 9 deletions packages/mermaid/src/diagram.spec.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -30,10 +30,10 @@ const getDummyDiagram = (id: string, title?: string): Awaited<ReturnType<Diagram

describe('diagram detection', () => {
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);
Expand All @@ -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');
});
Expand All @@ -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
Expand All @@ -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
Expand Down
28 changes: 12 additions & 16 deletions packages/mermaid/src/diagrams/info/info.spec.ts
Original file line number Diff line number Diff line change
@@ -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.'
);
});
});
7 changes: 3 additions & 4 deletions packages/mermaid/src/diagrams/info/infoParser.ts
Original file line number Diff line number Diff line change
@@ -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<void> => {
const ast: Info = await parse('info', input);
log.debug(ast);
},
};
Loading

0 comments on commit 969e2c1

Please sign in to comment.