Merge branch next into add-pie-langium-parser

This commit is contained in:
Reda Al Sulais
2024-01-27 20:55:15 +03:00
11 changed files with 115 additions and 173 deletions

View File

@@ -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<DiagramMetadata, 'title'> = {}) {
this.text = encodeEntities(text);
this.text += '\n';
const cnf = configApi.getConfig();
try {
this.type = detectType(text, cnf);
} catch (e) {
this.type = 'error';
this.detectError = e as UnknownDiagramError;
}
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) {
// The parser.parser.yy is only present in JISON parsers. So, we'll only set if required.
this.parser.parser.yy = this.db;
}
this.init = diagram.init;
this.parse();
}
parse() {
if (this.detectError) {
throw this.detectError;
}
this.db.clear?.();
public static async fromText(text: string, metadata: Pick<DiagramMetadata, 'title'> = {}) {
const config = configApi.getConfig();
this.init?.(config);
const type = detectType(text, config);
text = encodeEntities(text) + '\n';
try {
getDiagram(type);
} catch (e) {
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 { 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.
parser.parser.yy = db;
}
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<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);
};

View File

@@ -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', () => {

View File

@@ -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 };
}

View File

@@ -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<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);
@@ -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

View File

@@ -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.'
);
});
});

View File

@@ -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);
},
};

View File

@@ -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."'
);
});

View File

@@ -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<void> => {
const ast: Packet = await parse('packet', input);
log.debug(ast);
populate(ast);
},

View File

@@ -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

View File

@@ -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<DiagramMetadata, 'title'> = {}) => {
const { code } = preprocessDiagram(text);
return getDiagramFromTextInternal(code, metadata);
return Diagram.fromText(code, metadata);
};
/**

View File

@@ -1,38 +1,41 @@
import type { LangiumParser, ParseResult } from 'langium';
import type { Info, Packet, Pie } from './index.js';
import { createInfoServices, createPacketServices, createPieServices } from './language/index.js';
export type DiagramAST = Info | Packet | Pie;
const parsers: Record<string, LangiumParser> = {};
const initializers = {
info: () => {
// Will have to make parse async to use this. Can try later...
// const { createInfoServices } = await import('./language/info/index.js');
parsers['info'] = createInfoServices().Info.parser.LangiumParser;
info: async () => {
const { createInfoServices } = await import('./language/info/index.js');
const parser = createInfoServices().Info.parser.LangiumParser;
parsers['info'] = parser;
},
packet: () => {
parsers['packet'] = createPacketServices().Packet.parser.LangiumParser;
packet: async () => {
const { createPacketServices } = await import('./language/packet/index.js');
const parser = createPacketServices().Packet.parser.LangiumParser;
parsers['packet'] = parser;
},
pie: () => {
parsers['pie'] = createPieServices().Pie.parser.LangiumParser;
pie: async () => {
const { createPieServices } = await import('./language/pie/index.js');
const parser = createPieServices().Pie.parser.LangiumParser;
parsers['pie'] = parser;
},
} as const;
export function parse(diagramType: 'info', text: string): Info;
export function parse(diagramType: 'packet', text: string): Packet;
export function parse(diagramType: 'pie', text: string): Pie;
export function parse<T extends DiagramAST>(
export async function parse(diagramType: 'info', text: string): Promise<Info>;
export async function parse(diagramType: 'packet', text: string): Promise<Packet>;
export async function parse(diagramType: 'pie', text: string): Promise<Pie>;
export async function parse<T extends DiagramAST>(
diagramType: keyof typeof initializers,
text: string
): T {
): Promise<T> {
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<T> = parser.parse<T>(text);