Compare commits

...

3 Commits

Author SHA1 Message Date
Sidharth Vinod
5dc3850c52 Merge branch 'sidv/splitELK' into sidv/priorityToDiagrams
* sidv/splitELK:
  5043 Move ELK to standalone package
2023-11-20 12:08:17 +05:30
Sidharth Vinod
927217d77c 5043 Register internal diagrams before external 2023-11-19 08:35:05 +05:30
Sidharth Vinod
b5f3cdc0b0 feat: 5043 Add priority support for registered diagrams
Allows external diagrams to override internal diagrams, if necessary.
This will help move ELK to a different package, without completely breaking rendering, by falling back to dagre, and supporting ELK if it's registered as an external diagram.
2023-11-18 21:12:30 +05:30
9 changed files with 86 additions and 22 deletions

View File

@@ -1,7 +1,7 @@
import * as configApi from './config.js'; import * as configApi from './config.js';
import { log } from './logger.js'; import { log } from './logger.js';
import { getDiagram, registerDiagram } from './diagram-api/diagramAPI.js'; import { getDiagram, registerDiagram } from './diagram-api/diagramAPI.js';
import { detectType, getDiagramLoader } from './diagram-api/detectType.js'; import { detectType, getDiagramLoaderAndPriority } from './diagram-api/detectType.js';
import { UnknownDiagramError } from './errors.js'; import { UnknownDiagramError } from './errors.js';
import { encodeEntities } from './utils.js'; import { encodeEntities } from './utils.js';
@@ -92,14 +92,14 @@ export const getDiagramFromText = async (
// Trying to find the diagram // Trying to find the diagram
getDiagram(type); getDiagram(type);
} catch (error) { } catch (error) {
const loader = getDiagramLoader(type); const { loader, priority } = getDiagramLoaderAndPriority(type);
if (!loader) { if (!loader) {
throw new UnknownDiagramError(`Diagram ${type} not found.`); throw new UnknownDiagramError(`Diagram ${type} not found.`);
} }
// Diagram not available, loading it. // Diagram not available, loading it.
// new diagram will try getDiagram again and if fails then it is a valid throw // new diagram will try getDiagram again and if fails then it is a valid throw
const { id, diagram } = await loader(); const { id, diagram } = await loader();
registerDiagram(id, diagram); registerDiagram(id, diagram, priority);
} }
return new Diagram(text, metadata); return new Diagram(text, metadata);
}; };

View File

@@ -61,23 +61,37 @@ export const detectType = function (text: string, config?: MermaidConfig): strin
* The first detector to return `true` is the diagram that will be loaded * The first detector to return `true` is the diagram that will be loaded
* and used, so put more specific detectors at the beginning! * and used, so put more specific detectors at the beginning!
* *
* If two diagrams are registered with the same id,
* the one with higher `priority` property will be used.
*
* @param diagrams - Diagrams to lazy load, and their detectors, in order of importance. * @param diagrams - Diagrams to lazy load, and their detectors, in order of importance.
*/ */
export const registerLazyLoadedDiagrams = (...diagrams: ExternalDiagramDefinition[]) => { export const registerLazyLoadedDiagrams = (...diagrams: ExternalDiagramDefinition[]) => {
for (const { id, detector, loader } of diagrams) { for (const { id, detector, priority, loader } of diagrams) {
addDetector(id, detector, loader); addDetector(id, detector, priority ?? 0, loader);
} }
}; };
export const addDetector = (key: string, detector: DiagramDetector, loader?: DiagramLoader) => { export const addDetector = (
if (detectors[key]) { key: string,
log.error(`Detector with key ${key} already exists`); detector: DiagramDetector,
} else { priority: number,
detectors[key] = { detector, loader }; loader?: DiagramLoader
) => {
if (detectors[key] && priority <= detectors[key].priority) {
log.error(
`Detector with key ${key} already exists with priority ${detectors[key].priority}. Cannot add new detector with priority ${priority}`
);
return;
} }
log.debug(`Detector with key ${key} added${loader ? ' with loader' : ''}`);
detectors[key] = { detector, loader, priority };
log.debug(
`Detector with key ${key} added with priority ${priority} ${loader ? 'and loader' : ''}`
);
}; };
export const getDiagramLoader = (key: string) => { export const getDiagramLoaderAndPriority = (key: string) => {
return detectors[key].loader; const { loader, priority } = detectors[key];
return { loader, priority };
}; };

View File

@@ -31,7 +31,7 @@ export const addDiagrams = () => {
// This is added here to avoid race-conditions. // This is added here to avoid race-conditions.
// We could optimize the loading logic somehow. // We could optimize the loading logic somehow.
hasLoadedDiagrams = true; hasLoadedDiagrams = true;
registerDiagram('error', errorDiagram, (text) => { registerDiagram('error', errorDiagram, 0, (text) => {
return text.toLowerCase().trim() === 'error'; return text.toLowerCase().trim() === 'error';
}); });
registerDiagram( registerDiagram(
@@ -60,6 +60,7 @@ export const addDiagrams = () => {
}, },
init: () => null, // no op init: () => null, // no op
}, },
0,
(text) => { (text) => {
return text.toLowerCase().trimStart().startsWith('---'); return text.toLowerCase().trimStart().startsWith('---');
} }

View File

@@ -47,6 +47,7 @@ describe('DiagramAPI', () => {
}, },
styles: {}, styles: {},
}, },
0,
detector detector
); );
expect(getDiagram('loki')).not.toBeNull(); expect(getDiagram('loki')).not.toBeNull();

View File

@@ -29,7 +29,7 @@ export const getCommonDb = () => {
return _commonDb; return _commonDb;
}; };
const diagrams: Record<string, DiagramDefinition> = {}; const diagrams: Record<string, DiagramDefinition & { priority: number }> = {};
export interface Detectors { export interface Detectors {
[key: string]: DiagramDetector; [key: string]: DiagramDetector;
} }
@@ -37,7 +37,8 @@ export interface Detectors {
/** /**
* Registers the given diagram with Mermaid. * Registers the given diagram with Mermaid.
* *
* Can be used for third-party custom diagrams. * To be used internally by Mermaid.
* Use `mermaid.registerExternalDiagrams` to register external diagrams.
* *
* @param id - A unique ID for the given diagram. * @param id - A unique ID for the given diagram.
* @param diagram - The diagram definition. * @param diagram - The diagram definition.
@@ -46,14 +47,17 @@ export interface Detectors {
export const registerDiagram = ( export const registerDiagram = (
id: string, id: string,
diagram: DiagramDefinition, diagram: DiagramDefinition,
priority: number,
detector?: DiagramDetector detector?: DiagramDetector
) => { ) => {
if (diagrams[id]) { if (diagrams[id] && priority <= diagrams[id].priority) {
throw new Error(`Diagram ${id} already registered.`); throw new Error(
`Diagram ${id} already registered with priority ${diagrams[id].priority}. Cannot add new diagram with priority ${priority}`
);
} }
diagrams[id] = diagram; diagrams[id] = { ...diagram, priority };
if (detector) { if (detector) {
addDetector(id, detector); addDetector(id, detector, priority);
} }
addStylesForDiagram(id, diagram.styles); addStylesForDiagram(id, diagram.styles);

View File

@@ -6,7 +6,7 @@ export const loadRegisteredDiagrams = async () => {
log.debug(`Loading registered diagrams`); log.debug(`Loading registered diagrams`);
// Load all lazy loaded diagrams in parallel // Load all lazy loaded diagrams in parallel
const results = await Promise.allSettled( const results = await Promise.allSettled(
Object.entries(detectors).map(async ([key, { detector, loader }]) => { Object.entries(detectors).map(async ([key, { detector, loader, priority }]) => {
if (loader) { if (loader) {
try { try {
getDiagram(key); getDiagram(key);
@@ -14,7 +14,7 @@ export const loadRegisteredDiagrams = async () => {
try { try {
// Register diagram if it is not already registered // Register diagram if it is not already registered
const { diagram, id } = await loader(); const { diagram, id } = await loader();
registerDiagram(id, diagram, detector); registerDiagram(id, diagram, priority, detector);
} catch (err) { } catch (err) {
// Remove failed diagram from detectors // Remove failed diagram from detectors
log.error(`Failed to load external diagram with key ${key}. Removing from detectors.`); log.error(`Failed to load external diagram with key ${key}. Removing from detectors.`);

View File

@@ -76,13 +76,23 @@ export interface DiagramDefinition {
export interface DetectorRecord { export interface DetectorRecord {
detector: DiagramDetector; detector: DiagramDetector;
priority: number;
loader?: DiagramLoader; loader?: DiagramLoader;
} }
/**
* External diagrams, which are not bundled with mermaid should expose the following to be registered using the `mermaid.registerExternalDiagrams` function.
*
* @param id - An ID for the given diagram. If two diagrams are registered with the same ID, the one with the higher priority will be used.
* @param detector - Function that returns `true` if a given mermaid text satisfies with this diagram definition.
* @param loader - Function that returns a promise of the diagram definition.
* @param priority - The priority of the diagram. Optional, defaults to 0.
*/
export interface ExternalDiagramDefinition { export interface ExternalDiagramDefinition {
id: string; id: string;
detector: DiagramDetector; detector: DiagramDetector;
loader: DiagramLoader; loader: DiagramLoader;
priority?: number;
} }
export type DiagramDetector = (text: string, config?: MermaidConfig) => boolean; export type DiagramDetector = (text: string, config?: MermaidConfig) => boolean;

View File

@@ -21,6 +21,7 @@ describe('diagram detection', () => {
addDetector( addDetector(
'loki', 'loki',
(str) => str.startsWith('loki'), (str) => str.startsWith('loki'),
0,
() => () =>
Promise.resolve({ Promise.resolve({
id: 'loki', id: 'loki',
@@ -45,6 +46,37 @@ describe('diagram detection', () => {
expect(diagram.type).toBe('loki'); expect(diagram.type).toBe('loki');
}); });
test('should allow external diagrams to override internal ones with same ID', async () => {
addDetector(
'flowchart-elk',
(str) => str.startsWith('flowchart-elk'),
1,
() =>
Promise.resolve({
id: 'flowchart-elk',
diagram: {
db: {
getDiagramTitle: () => 'overridden',
},
parser: {
parse: () => {
// no-op
},
},
renderer: {
draw: () => {
// no-op
},
},
styles: {},
},
})
);
const diagram = (await getDiagramFromText('flowchart-elk TD; A-->B')) as Diagram;
expect(diagram).toBeInstanceOf(Diagram);
expect(diagram.db.getDiagramTitle?.()).toBe('overridden');
});
test('should throw the right error for incorrect diagram', async () => { test('should throw the right error for incorrect diagram', async () => {
await expect(getDiagramFromText('graph TD; A-->')).rejects.toThrowErrorMatchingInlineSnapshot(` await expect(getDiagramFromText('graph TD; A-->')).rejects.toThrowErrorMatchingInlineSnapshot(`
"Parse error on line 2: "Parse error on line 2:

View File

@@ -15,6 +15,7 @@ import { isDetailedError } from './utils.js';
import type { DetailedError } from './utils.js'; import type { DetailedError } from './utils.js';
import type { ExternalDiagramDefinition } from './diagram-api/types.js'; import type { ExternalDiagramDefinition } from './diagram-api/types.js';
import type { UnknownDiagramError } from './errors.js'; import type { UnknownDiagramError } from './errors.js';
import { addDiagrams } from './diagram-api/diagram-orchestration.js';
export type { export type {
MermaidConfig, MermaidConfig,
@@ -243,6 +244,7 @@ const registerExternalDiagrams = async (
lazyLoad?: boolean; lazyLoad?: boolean;
} = {} } = {}
) => { ) => {
addDiagrams();
registerLazyLoadedDiagrams(...diagrams); registerLazyLoadedDiagrams(...diagrams);
if (lazyLoad === false) { if (lazyLoad === false) {
await loadRegisteredDiagrams(); await loadRegisteredDiagrams();