mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-09-16 22:09:57 +02:00
refactor: Move directive processing before parsing
Directives and fronmatter will be preprocessed and removed from the text before parsing.
This commit is contained in:
@@ -16,4 +16,4 @@
|
|||||||
|
|
||||||
#### Defined in
|
#### Defined in
|
||||||
|
|
||||||
[mermaidAPI.ts:78](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L78)
|
[mermaidAPI.ts:76](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L76)
|
||||||
|
@@ -39,7 +39,7 @@ bindFunctions?.(div); // To call bindFunctions only if it's present.
|
|||||||
|
|
||||||
#### Defined in
|
#### Defined in
|
||||||
|
|
||||||
[mermaidAPI.ts:98](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L98)
|
[mermaidAPI.ts:96](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L96)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -51,4 +51,4 @@ The svg code for the rendered graph.
|
|||||||
|
|
||||||
#### Defined in
|
#### Defined in
|
||||||
|
|
||||||
[mermaidAPI.ts:88](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L88)
|
[mermaidAPI.ts:86](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L86)
|
||||||
|
@@ -25,13 +25,13 @@ Renames and re-exports [mermaidAPI](mermaidAPI.md#mermaidapi)
|
|||||||
|
|
||||||
#### Defined in
|
#### Defined in
|
||||||
|
|
||||||
[mermaidAPI.ts:82](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L82)
|
[mermaidAPI.ts:80](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L80)
|
||||||
|
|
||||||
## Variables
|
## Variables
|
||||||
|
|
||||||
### mermaidAPI
|
### mermaidAPI
|
||||||
|
|
||||||
• `Const` **mermaidAPI**: `Readonly`<{ `defaultConfig`: `MermaidConfig` = configApi.defaultConfig; `getConfig`: () => `MermaidConfig` = configApi.getConfig; `getDiagramFromText`: (`text`: `string`) => `Promise`<`Diagram`> ; `getSiteConfig`: () => `MermaidConfig` = configApi.getSiteConfig; `globalReset`: () => `void` ; `initialize`: (`options`: `MermaidConfig`) => `void` ; `parse`: (`text`: `string`, `parseOptions?`: [`ParseOptions`](../interfaces/mermaidAPI.ParseOptions.md)) => `Promise`<`boolean`> ; `parseDirective`: (`p`: `any`, `statement`: `string`, `context`: `string`, `type`: `string`) => `void` ; `render`: (`id`: `string`, `text`: `string`, `svgContainingElement?`: `Element`) => `Promise`<[`RenderResult`](../interfaces/mermaidAPI.RenderResult.md)> ; `reset`: () => `void` ; `setConfig`: (`conf`: `MermaidConfig`) => `MermaidConfig` = configApi.setConfig; `updateSiteConfig`: (`conf`: `MermaidConfig`) => `MermaidConfig` = configApi.updateSiteConfig }>
|
• `Const` **mermaidAPI**: `Readonly`<{ `defaultConfig`: `MermaidConfig` = configApi.defaultConfig; `getConfig`: () => `MermaidConfig` = configApi.getConfig; `getDiagramFromText`: (`text`: `string`, `metadata`: { `title?`: `string` }) => `Promise`<`Diagram`> ; `getSiteConfig`: () => `MermaidConfig` = configApi.getSiteConfig; `globalReset`: () => `void` ; `initialize`: (`options`: `MermaidConfig`) => `void` ; `parse`: (`text`: `string`, `parseOptions?`: [`ParseOptions`](../interfaces/mermaidAPI.ParseOptions.md)) => `Promise`<`boolean`> ; `render`: (`id`: `string`, `text`: `string`, `svgContainingElement?`: `Element`) => `Promise`<[`RenderResult`](../interfaces/mermaidAPI.RenderResult.md)> ; `reset`: () => `void` ; `setConfig`: (`conf`: `MermaidConfig`) => `MermaidConfig` = configApi.setConfig; `updateSiteConfig`: (`conf`: `MermaidConfig`) => `MermaidConfig` = configApi.updateSiteConfig }>
|
||||||
|
|
||||||
## mermaidAPI configuration defaults
|
## mermaidAPI configuration defaults
|
||||||
|
|
||||||
@@ -96,7 +96,7 @@ mermaid.initialize(config);
|
|||||||
|
|
||||||
#### Defined in
|
#### Defined in
|
||||||
|
|
||||||
[mermaidAPI.ts:673](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L673)
|
[mermaidAPI.ts:662](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L662)
|
||||||
|
|
||||||
## Functions
|
## Functions
|
||||||
|
|
||||||
@@ -127,7 +127,7 @@ Return the last node appended
|
|||||||
|
|
||||||
#### Defined in
|
#### Defined in
|
||||||
|
|
||||||
[mermaidAPI.ts:310](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L310)
|
[mermaidAPI.ts:318](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L318)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -153,7 +153,7 @@ the cleaned up svgCode
|
|||||||
|
|
||||||
#### Defined in
|
#### Defined in
|
||||||
|
|
||||||
[mermaidAPI.ts:256](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L256)
|
[mermaidAPI.ts:264](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L264)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -179,7 +179,7 @@ the string with all the user styles
|
|||||||
|
|
||||||
#### Defined in
|
#### Defined in
|
||||||
|
|
||||||
[mermaidAPI.ts:185](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L185)
|
[mermaidAPI.ts:193](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L193)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -202,7 +202,7 @@ the string with all the user styles
|
|||||||
|
|
||||||
#### Defined in
|
#### Defined in
|
||||||
|
|
||||||
[mermaidAPI.ts:233](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L233)
|
[mermaidAPI.ts:241](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L241)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -229,7 +229,7 @@ with an enclosing block that has each of the cssClasses followed by !important;
|
|||||||
|
|
||||||
#### Defined in
|
#### Defined in
|
||||||
|
|
||||||
[mermaidAPI.ts:169](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L169)
|
[mermaidAPI.ts:177](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L177)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -249,7 +249,7 @@ with an enclosing block that has each of the cssClasses followed by !important;
|
|||||||
|
|
||||||
#### Defined in
|
#### Defined in
|
||||||
|
|
||||||
[mermaidAPI.ts:155](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L155)
|
[mermaidAPI.ts:163](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L163)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -269,7 +269,7 @@ with an enclosing block that has each of the cssClasses followed by !important;
|
|||||||
|
|
||||||
#### Defined in
|
#### Defined in
|
||||||
|
|
||||||
[mermaidAPI.ts:126](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L126)
|
[mermaidAPI.ts:134](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L134)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -295,7 +295,7 @@ Put the svgCode into an iFrame. Return the iFrame code
|
|||||||
|
|
||||||
#### Defined in
|
#### Defined in
|
||||||
|
|
||||||
[mermaidAPI.ts:287](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L287)
|
[mermaidAPI.ts:295](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L295)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -320,4 +320,4 @@ Remove any existing elements from the given document
|
|||||||
|
|
||||||
#### Defined in
|
#### Defined in
|
||||||
|
|
||||||
[mermaidAPI.ts:360](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L360)
|
[mermaidAPI.ts:368](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L368)
|
||||||
|
@@ -2,9 +2,7 @@ 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, getDiagramLoader } from './diagram-api/detectType.js';
|
||||||
import { extractFrontMatter } from './diagram-api/frontmatter.js';
|
|
||||||
import { UnknownDiagramError } from './errors.js';
|
import { UnknownDiagramError } from './errors.js';
|
||||||
import { cleanupComments } from './diagram-api/comments.js';
|
|
||||||
import type { DetailedError } from './utils.js';
|
import type { DetailedError } from './utils.js';
|
||||||
import type { DiagramDefinition } from './diagram-api/types.js';
|
import type { DiagramDefinition } from './diagram-api/types.js';
|
||||||
|
|
||||||
@@ -22,7 +20,7 @@ export class Diagram {
|
|||||||
private init?: DiagramDefinition['init'];
|
private init?: DiagramDefinition['init'];
|
||||||
|
|
||||||
private detectError?: UnknownDiagramError;
|
private detectError?: UnknownDiagramError;
|
||||||
constructor(public text: string) {
|
constructor(public text: string, public metadata: { title?: string } = {}) {
|
||||||
this.text += '\n';
|
this.text += '\n';
|
||||||
const cnf = configApi.getConfig();
|
const cnf = configApi.getConfig();
|
||||||
try {
|
try {
|
||||||
@@ -37,19 +35,6 @@ export class Diagram {
|
|||||||
this.db = diagram.db;
|
this.db = diagram.db;
|
||||||
this.renderer = diagram.renderer;
|
this.renderer = diagram.renderer;
|
||||||
this.parser = diagram.parser;
|
this.parser = diagram.parser;
|
||||||
const originalParse = this.parser.parse.bind(this.parser);
|
|
||||||
// Wrap the jison parse() method to handle extracting frontmatter.
|
|
||||||
//
|
|
||||||
// This can't be done in this.parse() because some code
|
|
||||||
// directly calls diagram.parser.parse(), bypassing this.parse().
|
|
||||||
//
|
|
||||||
// Similarly, we can't do this in getDiagramFromText() because some code
|
|
||||||
// calls diagram.db.clear(), which would reset anything set by
|
|
||||||
// extractFrontMatter().
|
|
||||||
|
|
||||||
this.parser.parse = (text: string) =>
|
|
||||||
originalParse(cleanupComments(extractFrontMatter(text, this.db, configApi.addDirective)));
|
|
||||||
|
|
||||||
this.parser.parser.yy = this.db;
|
this.parser.parser.yy = this.db;
|
||||||
this.init = diagram.init;
|
this.init = diagram.init;
|
||||||
this.parse();
|
this.parse();
|
||||||
@@ -60,7 +45,15 @@ export class Diagram {
|
|||||||
throw this.detectError;
|
throw this.detectError;
|
||||||
}
|
}
|
||||||
this.db.clear?.();
|
this.db.clear?.();
|
||||||
this.init?.(configApi.getConfig());
|
const config = configApi.getConfig();
|
||||||
|
this.init?.(config);
|
||||||
|
// These 2 blocks were added for legacy compatibility. Do not add more such blocks. Use frontmatter instead.
|
||||||
|
if (this.metadata.title) {
|
||||||
|
this.db.setDiagramTitle?.(this.metadata.title);
|
||||||
|
}
|
||||||
|
if (config.wrap) {
|
||||||
|
this.db.setWrap?.(config.wrap);
|
||||||
|
}
|
||||||
this.parser.parse(this.text);
|
this.parser.parse(this.text);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,7 +79,10 @@ export class Diagram {
|
|||||||
* @throws {@link UnknownDiagramError} if the diagram type can not be found.
|
* @throws {@link UnknownDiagramError} if the diagram type can not be found.
|
||||||
* @privateRemarks This is exported as part of the public mermaidAPI.
|
* @privateRemarks This is exported as part of the public mermaidAPI.
|
||||||
*/
|
*/
|
||||||
export const getDiagramFromText = async (text: string): Promise<Diagram> => {
|
export const getDiagramFromText = async (
|
||||||
|
text: string,
|
||||||
|
metadata: { title?: string } = {}
|
||||||
|
): Promise<Diagram> => {
|
||||||
const type = detectType(text, configApi.getConfig());
|
const type = detectType(text, configApi.getConfig());
|
||||||
try {
|
try {
|
||||||
// Trying to find the diagram
|
// Trying to find the diagram
|
||||||
@@ -101,5 +97,5 @@ export const getDiagramFromText = async (text: string): Promise<Diagram> => {
|
|||||||
const { id, diagram } = await loader();
|
const { id, diagram } = await loader();
|
||||||
registerDiagram(id, diagram);
|
registerDiagram(id, diagram);
|
||||||
}
|
}
|
||||||
return new Diagram(text);
|
return new Diagram(text, metadata);
|
||||||
};
|
};
|
||||||
|
@@ -13,7 +13,6 @@ export const mermaidAPI = {
|
|||||||
svg: '<svg></svg>',
|
svg: '<svg></svg>',
|
||||||
}),
|
}),
|
||||||
parse: mAPI.parse,
|
parse: mAPI.parse,
|
||||||
parseDirective: vi.fn(),
|
|
||||||
initialize: vi.fn(),
|
initialize: vi.fn(),
|
||||||
getConfig: configApi.getConfig,
|
getConfig: configApi.getConfig,
|
||||||
setConfig: configApi.setConfig,
|
setConfig: configApi.setConfig,
|
||||||
|
@@ -3,7 +3,7 @@ import { log } from './logger.js';
|
|||||||
import theme from './themes/index.js';
|
import theme from './themes/index.js';
|
||||||
import config from './defaultConfig.js';
|
import config from './defaultConfig.js';
|
||||||
import type { MermaidConfig } from './config.type.js';
|
import type { MermaidConfig } from './config.type.js';
|
||||||
import { sanitizeDirective } from './utils.js';
|
import { sanitizeDirective } from './utils/sanitizeDirective.js';
|
||||||
|
|
||||||
export const defaultConfig: MermaidConfig = Object.freeze(config);
|
export const defaultConfig: MermaidConfig = Object.freeze(config);
|
||||||
|
|
||||||
|
@@ -4,5 +4,5 @@
|
|||||||
* @returns cleaned text
|
* @returns cleaned text
|
||||||
*/
|
*/
|
||||||
export const cleanupComments = (text: string): string => {
|
export const cleanupComments = (text: string): string => {
|
||||||
return text.trimStart().replace(/^\s*%%(?!{)[^\n]+\n?/gm, '');
|
return text.replace(/^\s*%%(?!{)[^\n]+\n?/gm, '').trimStart();
|
||||||
};
|
};
|
||||||
|
@@ -6,7 +6,6 @@ import { setupGraphViewbox as _setupGraphViewbox } from '../setupGraphViewbox.js
|
|||||||
import { addStylesForDiagram } from '../styles.js';
|
import { addStylesForDiagram } from '../styles.js';
|
||||||
import type { DiagramDefinition, DiagramDetector } from './types.js';
|
import type { DiagramDefinition, DiagramDetector } from './types.js';
|
||||||
import * as _commonDb from '../commonDb.js';
|
import * as _commonDb from '../commonDb.js';
|
||||||
import { parseDirective as _parseDirective } from '../directiveUtils.js';
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Packaging and exposing resources for external diagrams so that they can import
|
Packaging and exposing resources for external diagrams so that they can import
|
||||||
@@ -21,8 +20,6 @@ export const setupGraphViewbox = _setupGraphViewbox;
|
|||||||
export const getCommonDb = () => {
|
export const getCommonDb = () => {
|
||||||
return _commonDb;
|
return _commonDb;
|
||||||
};
|
};
|
||||||
export const parseDirective = (p: any, statement: string, context: string, type: string) =>
|
|
||||||
_parseDirective(p, statement, context, type);
|
|
||||||
|
|
||||||
const diagrams: Record<string, DiagramDefinition> = {};
|
const diagrams: Record<string, DiagramDefinition> = {};
|
||||||
export interface Detectors {
|
export interface Detectors {
|
||||||
@@ -52,17 +49,17 @@ export const registerDiagram = (
|
|||||||
}
|
}
|
||||||
addStylesForDiagram(id, diagram.styles);
|
addStylesForDiagram(id, diagram.styles);
|
||||||
|
|
||||||
if (diagram.injectUtils) {
|
diagram.injectUtils?.(
|
||||||
diagram.injectUtils(
|
log,
|
||||||
log,
|
setLogLevel,
|
||||||
setLogLevel,
|
getConfig,
|
||||||
getConfig,
|
sanitizeText,
|
||||||
sanitizeText,
|
setupGraphViewbox,
|
||||||
setupGraphViewbox,
|
getCommonDb(),
|
||||||
getCommonDb(),
|
() => {
|
||||||
parseDirective
|
// parseDirective is removed. This is a no-op for legacy support.
|
||||||
);
|
}
|
||||||
}
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getDiagram = (name: string): DiagramDefinition => {
|
export const getDiagram = (name: string): DiagramDefinition => {
|
||||||
|
@@ -1,84 +1,139 @@
|
|||||||
import { vi } from 'vitest';
|
|
||||||
import { extractFrontMatter } from './frontmatter.js';
|
import { extractFrontMatter } from './frontmatter.js';
|
||||||
|
|
||||||
const dbMock = () => ({ setDiagramTitle: vi.fn() });
|
|
||||||
const setConfigMock = vi.fn();
|
|
||||||
|
|
||||||
describe('extractFrontmatter', () => {
|
describe('extractFrontmatter', () => {
|
||||||
beforeEach(() => {
|
|
||||||
setConfigMock.mockClear();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns text unchanged if no frontmatter', () => {
|
it('returns text unchanged if no frontmatter', () => {
|
||||||
expect(extractFrontMatter('diagram', dbMock())).toEqual('diagram');
|
expect(extractFrontMatter('diagram')).toMatchInlineSnapshot(`
|
||||||
|
{
|
||||||
|
"metadata": {},
|
||||||
|
"text": "diagram",
|
||||||
|
}
|
||||||
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns text unchanged if frontmatter lacks closing delimiter', () => {
|
it('returns text unchanged if frontmatter lacks closing delimiter', () => {
|
||||||
const text = `---\ntitle: foo\ndiagram`;
|
const text = `---\ntitle: foo\ndiagram`;
|
||||||
expect(extractFrontMatter(text, dbMock())).toEqual(text);
|
expect(extractFrontMatter(text)).toMatchInlineSnapshot(`
|
||||||
|
{
|
||||||
|
"metadata": {},
|
||||||
|
"text": "---
|
||||||
|
title: foo
|
||||||
|
diagram",
|
||||||
|
}
|
||||||
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('handles empty frontmatter', () => {
|
it('handles empty frontmatter', () => {
|
||||||
const db = dbMock();
|
|
||||||
const text = `---\n\n---\ndiagram`;
|
const text = `---\n\n---\ndiagram`;
|
||||||
expect(extractFrontMatter(text, db)).toEqual('diagram');
|
expect(extractFrontMatter(text)).toMatchInlineSnapshot(`
|
||||||
expect(db.setDiagramTitle).not.toHaveBeenCalled();
|
{
|
||||||
|
"metadata": {},
|
||||||
|
"text": "diagram",
|
||||||
|
}
|
||||||
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('handles frontmatter without mappings', () => {
|
it('handles frontmatter without mappings', () => {
|
||||||
const db = dbMock();
|
expect(extractFrontMatter(`---\n1\n---\ndiagram`)).toMatchInlineSnapshot(`
|
||||||
const text = `---\n1\n---\ndiagram`;
|
{
|
||||||
expect(extractFrontMatter(text, db)).toEqual('diagram');
|
"metadata": {},
|
||||||
expect(db.setDiagramTitle).not.toHaveBeenCalled();
|
"text": "diagram",
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
expect(extractFrontMatter(`---\n-1\n-2\n---\ndiagram`)).toMatchInlineSnapshot(`
|
||||||
|
{
|
||||||
|
"metadata": {},
|
||||||
|
"text": "diagram",
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
expect(extractFrontMatter(`---\nnull\n---\ndiagram`)).toMatchInlineSnapshot(`
|
||||||
|
{
|
||||||
|
"metadata": {},
|
||||||
|
"text": "diagram",
|
||||||
|
}
|
||||||
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not try to parse frontmatter at the end', () => {
|
it('does not try to parse frontmatter at the end', () => {
|
||||||
const db = dbMock();
|
|
||||||
const text = `diagram\n---\ntitle: foo\n---\n`;
|
const text = `diagram\n---\ntitle: foo\n---\n`;
|
||||||
expect(extractFrontMatter(text, db)).toEqual(text);
|
expect(extractFrontMatter(text)).toMatchInlineSnapshot(`
|
||||||
expect(db.setDiagramTitle).not.toHaveBeenCalled();
|
{
|
||||||
|
"metadata": {},
|
||||||
|
"text": "diagram
|
||||||
|
---
|
||||||
|
title: foo
|
||||||
|
---
|
||||||
|
",
|
||||||
|
}
|
||||||
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('handles frontmatter with multiple delimiters', () => {
|
it('handles frontmatter with multiple delimiters', () => {
|
||||||
const db = dbMock();
|
|
||||||
const text = `---\ntitle: foo---bar\n---\ndiagram\n---\ntest`;
|
const text = `---\ntitle: foo---bar\n---\ndiagram\n---\ntest`;
|
||||||
expect(extractFrontMatter(text, db)).toEqual('diagram\n---\ntest');
|
expect(extractFrontMatter(text)).toMatchInlineSnapshot(`
|
||||||
expect(db.setDiagramTitle).toHaveBeenCalledWith('foo---bar');
|
{
|
||||||
|
"metadata": {
|
||||||
|
"title": "foo---bar",
|
||||||
|
},
|
||||||
|
"text": "diagram
|
||||||
|
---
|
||||||
|
test",
|
||||||
|
}
|
||||||
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('handles frontmatter with multi-line string and multiple delimiters', () => {
|
it('handles frontmatter with multi-line string and multiple delimiters', () => {
|
||||||
const db = dbMock();
|
|
||||||
const text = `---\ntitle: |\n multi-line string\n ---\n---\ndiagram`;
|
const text = `---\ntitle: |\n multi-line string\n ---\n---\ndiagram`;
|
||||||
expect(extractFrontMatter(text, db)).toEqual('diagram');
|
expect(extractFrontMatter(text)).toMatchInlineSnapshot(`
|
||||||
expect(db.setDiagramTitle).toHaveBeenCalledWith('multi-line string\n---\n');
|
{
|
||||||
|
"metadata": {
|
||||||
|
"title": "multi-line string
|
||||||
|
---
|
||||||
|
",
|
||||||
|
},
|
||||||
|
"text": "diagram",
|
||||||
|
}
|
||||||
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('handles frontmatter with title', () => {
|
it('handles frontmatter with title', () => {
|
||||||
const db = dbMock();
|
|
||||||
const text = `---\ntitle: foo\n---\ndiagram`;
|
const text = `---\ntitle: foo\n---\ndiagram`;
|
||||||
expect(extractFrontMatter(text, db)).toEqual('diagram');
|
expect(extractFrontMatter(text)).toMatchInlineSnapshot(`
|
||||||
expect(db.setDiagramTitle).toHaveBeenCalledWith('foo');
|
{
|
||||||
|
"metadata": {
|
||||||
|
"title": "foo",
|
||||||
|
},
|
||||||
|
"text": "diagram",
|
||||||
|
}
|
||||||
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('handles booleans in frontmatter properly', () => {
|
it('handles booleans in frontmatter properly', () => {
|
||||||
const db = dbMock();
|
|
||||||
const text = `---\ntitle: true\n---\ndiagram`;
|
const text = `---\ntitle: true\n---\ndiagram`;
|
||||||
expect(extractFrontMatter(text, db)).toEqual('diagram');
|
expect(extractFrontMatter(text)).toMatchInlineSnapshot(`
|
||||||
expect(db.setDiagramTitle).toHaveBeenCalledWith('true');
|
{
|
||||||
|
"metadata": {
|
||||||
|
"title": "true",
|
||||||
|
},
|
||||||
|
"text": "diagram",
|
||||||
|
}
|
||||||
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('ignores unspecified frontmatter keys', () => {
|
it('ignores unspecified frontmatter keys', () => {
|
||||||
const db = dbMock();
|
|
||||||
const text = `---\ninvalid: true\ntitle: foo\ntest: bar\n---\ndiagram`;
|
const text = `---\ninvalid: true\ntitle: foo\ntest: bar\n---\ndiagram`;
|
||||||
expect(extractFrontMatter(text, db)).toEqual('diagram');
|
expect(extractFrontMatter(text)).toMatchInlineSnapshot(`
|
||||||
expect(db.setDiagramTitle).toHaveBeenCalledWith('foo');
|
{
|
||||||
|
"metadata": {
|
||||||
|
"title": "foo",
|
||||||
|
},
|
||||||
|
"text": "diagram",
|
||||||
|
}
|
||||||
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throws exception for invalid YAML syntax', () => {
|
it('throws exception for invalid YAML syntax', () => {
|
||||||
const text = `---\n!!!\n---\ndiagram`;
|
const text = `---\n!!!\n---\ndiagram`;
|
||||||
expect(() => extractFrontMatter(text, dbMock())).toThrow(
|
expect(() => extractFrontMatter(text)).toThrow('tag suffix cannot contain exclamation marks');
|
||||||
'tag suffix cannot contain exclamation marks'
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('handles frontmatter with config', () => {
|
it('handles frontmatter with config', () => {
|
||||||
@@ -92,9 +147,25 @@ config:
|
|||||||
array: [1, 2, 3]
|
array: [1, 2, 3]
|
||||||
---
|
---
|
||||||
diagram`;
|
diagram`;
|
||||||
expect(extractFrontMatter(text, {}, setConfigMock)).toEqual('diagram');
|
expect(extractFrontMatter(text)).toMatchInlineSnapshot(`
|
||||||
expect(setConfigMock).toHaveBeenCalledWith({
|
{
|
||||||
graph: { string: 'hello', number: 14, boolean: false, array: [1, 2, 3] },
|
"metadata": {
|
||||||
});
|
"config": {
|
||||||
|
"graph": {
|
||||||
|
"array": [
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
3,
|
||||||
|
],
|
||||||
|
"boolean": false,
|
||||||
|
"number": 14,
|
||||||
|
"string": "hello",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"title": "hello",
|
||||||
|
},
|
||||||
|
"text": "diagram",
|
||||||
|
}
|
||||||
|
`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,6 +1,5 @@
|
|||||||
import type { MermaidConfig } from '../config.type.js';
|
import type { MermaidConfig } from '../config.type.js';
|
||||||
import { frontMatterRegex } from './regexes.js';
|
import { frontMatterRegex } from './regexes.js';
|
||||||
import type { DiagramDB } from './types.js';
|
|
||||||
// The "* as yaml" part is necessary for tree-shaking
|
// The "* as yaml" part is necessary for tree-shaking
|
||||||
import * as yaml from 'js-yaml';
|
import * as yaml from 'js-yaml';
|
||||||
|
|
||||||
@@ -11,43 +10,51 @@ interface FrontMatterMetadata {
|
|||||||
config?: MermaidConfig;
|
config?: MermaidConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface FrontMatterResult {
|
||||||
|
text: string;
|
||||||
|
metadata: FrontMatterMetadata;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extract and parse frontmatter from text, if present, and sets appropriate
|
* Extract and parse frontmatter from text, if present, and sets appropriate
|
||||||
* properties in the provided db.
|
* properties in the provided db.
|
||||||
* @param text - The text that may have a YAML frontmatter.
|
* @param text - The text that may have a YAML frontmatter.
|
||||||
* @param db - Diagram database, could be of any diagram.
|
|
||||||
* @param setDiagramConfig - Optional function to set diagram config.
|
|
||||||
* @returns text with frontmatter stripped out
|
* @returns text with frontmatter stripped out
|
||||||
*/
|
*/
|
||||||
export function extractFrontMatter(
|
export function extractFrontMatter(text: string): FrontMatterResult {
|
||||||
text: string,
|
|
||||||
db: DiagramDB,
|
|
||||||
setDiagramConfig?: (config: MermaidConfig) => void
|
|
||||||
): string {
|
|
||||||
const matches = text.match(frontMatterRegex);
|
const matches = text.match(frontMatterRegex);
|
||||||
if (!matches) {
|
if (!matches) {
|
||||||
return text;
|
return {
|
||||||
|
text,
|
||||||
|
metadata: {},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const parsed: FrontMatterMetadata = yaml.load(matches[1], {
|
let parsed: FrontMatterMetadata =
|
||||||
// To support config, we need JSON schema.
|
yaml.load(matches[1], {
|
||||||
// https://www.yaml.org/spec/1.2/spec.html#id2803231
|
// To support config, we need JSON schema.
|
||||||
schema: yaml.JSON_SCHEMA,
|
// https://www.yaml.org/spec/1.2/spec.html#id2803231
|
||||||
}) as FrontMatterMetadata;
|
schema: yaml.JSON_SCHEMA,
|
||||||
|
}) ?? {};
|
||||||
|
|
||||||
if (parsed?.title) {
|
// To handle runtime data type changes
|
||||||
// toString() is necessary because YAML could parse the title as a number/boolean
|
parsed = typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
|
||||||
db.setDiagramTitle?.(parsed.title.toString());
|
|
||||||
|
const metadata: FrontMatterMetadata = {};
|
||||||
|
|
||||||
|
// Only add properties that are explicitly supported, if they exist
|
||||||
|
if (parsed.displayMode) {
|
||||||
|
metadata.displayMode = parsed.displayMode.toString();
|
||||||
|
}
|
||||||
|
if (parsed.title) {
|
||||||
|
metadata.title = parsed.title.toString();
|
||||||
|
}
|
||||||
|
if (parsed.config) {
|
||||||
|
metadata.config = parsed.config;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parsed?.displayMode) {
|
return {
|
||||||
// toString() is necessary because YAML could parse the title as a number/boolean
|
text: text.slice(matches[0].length),
|
||||||
db.setDisplayMode?.(parsed.displayMode.toString());
|
metadata,
|
||||||
}
|
};
|
||||||
|
|
||||||
if (parsed?.config) {
|
|
||||||
setDiagramConfig?.(parsed.config);
|
|
||||||
}
|
|
||||||
|
|
||||||
return text.slice(matches[0].length);
|
|
||||||
}
|
}
|
||||||
|
@@ -29,6 +29,7 @@ export interface DiagramDB {
|
|||||||
getAccDescription?: () => string;
|
getAccDescription?: () => string;
|
||||||
|
|
||||||
setDisplayMode?: (title: string) => void;
|
setDisplayMode?: (title: string) => void;
|
||||||
|
setWrap?: (wrap: boolean) => void;
|
||||||
bindFunctions?: (element: Element) => void;
|
bindFunctions?: (element: Element) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,15 +84,6 @@ export interface ParserDefinition {
|
|||||||
parser: { yy: DiagramDB };
|
parser: { yy: DiagramDB };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Type for function parse directive from diagram code.
|
|
||||||
*
|
|
||||||
* @param statement -
|
|
||||||
* @param context -
|
|
||||||
* @param type -
|
|
||||||
*/
|
|
||||||
export type ParseDirectiveDefinition = (statement: string, context: string, type: string) => void;
|
|
||||||
|
|
||||||
export type HTML = d3.Selection<HTMLIFrameElement, unknown, Element | null, unknown>;
|
export type HTML = d3.Selection<HTMLIFrameElement, unknown, Element | null, unknown>;
|
||||||
|
|
||||||
export type SVG = d3.Selection<SVGSVGElement, unknown, Element | null, unknown>;
|
export type SVG = d3.Selection<SVGSVGElement, unknown, Element | null, unknown>;
|
||||||
|
@@ -1,82 +0,0 @@
|
|||||||
import * as configApi from './config.js';
|
|
||||||
import { log } from './logger.js';
|
|
||||||
|
|
||||||
let currentDirective: { type?: string; args?: any } | undefined = {};
|
|
||||||
|
|
||||||
export const parseDirective = function (
|
|
||||||
p: any,
|
|
||||||
statement: string,
|
|
||||||
context: string,
|
|
||||||
type: string
|
|
||||||
): void {
|
|
||||||
log.debug('parseDirective is being called', statement, context, type);
|
|
||||||
try {
|
|
||||||
if (statement !== undefined) {
|
|
||||||
statement = statement.trim();
|
|
||||||
switch (context) {
|
|
||||||
case 'open_directive':
|
|
||||||
currentDirective = {};
|
|
||||||
break;
|
|
||||||
case 'type_directive':
|
|
||||||
if (!currentDirective) {
|
|
||||||
throw new Error('currentDirective is undefined');
|
|
||||||
}
|
|
||||||
currentDirective.type = statement.toLowerCase();
|
|
||||||
break;
|
|
||||||
case 'arg_directive':
|
|
||||||
if (!currentDirective) {
|
|
||||||
throw new Error('currentDirective is undefined');
|
|
||||||
}
|
|
||||||
currentDirective.args = JSON.parse(statement);
|
|
||||||
break;
|
|
||||||
case 'close_directive':
|
|
||||||
handleDirective(p, currentDirective, type);
|
|
||||||
currentDirective = undefined;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
log.error(
|
|
||||||
`Error while rendering sequenceDiagram directive: ${statement} jison context: ${context}`
|
|
||||||
);
|
|
||||||
// @ts-ignore: TODO Fix ts errors
|
|
||||||
log.error(error.message);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDirective = function (p: any, directive: any, type: string): void {
|
|
||||||
log.info(`Directive type=${directive.type} with args:`, directive.args);
|
|
||||||
switch (directive.type) {
|
|
||||||
case 'init':
|
|
||||||
case 'initialize': {
|
|
||||||
['config'].forEach((prop) => {
|
|
||||||
if (directive.args[prop] !== undefined) {
|
|
||||||
if (type === 'flowchart-v2') {
|
|
||||||
type = 'flowchart';
|
|
||||||
}
|
|
||||||
directive.args[type] = directive.args[prop];
|
|
||||||
delete directive.args[prop];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
configApi.addDirective(directive.args);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'wrap':
|
|
||||||
case 'nowrap':
|
|
||||||
if (p && p['setWrap']) {
|
|
||||||
p.setWrap(directive.type === 'wrap');
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'themeCss':
|
|
||||||
log.warn('themeCss encountered');
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
log.warn(
|
|
||||||
`Unhandled directive: source: '%%{${directive.type}: ${JSON.stringify(
|
|
||||||
directive.args ? directive.args : {}
|
|
||||||
)}}%%`,
|
|
||||||
directive
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
@@ -23,14 +23,12 @@ import { attachFunctions } from './interactionDb.js';
|
|||||||
import { log, setLogLevel } from './logger.js';
|
import { log, setLogLevel } from './logger.js';
|
||||||
import getStyles from './styles.js';
|
import getStyles from './styles.js';
|
||||||
import theme from './themes/index.js';
|
import theme from './themes/index.js';
|
||||||
import utils from './utils.js';
|
|
||||||
import DOMPurify from 'dompurify';
|
import DOMPurify from 'dompurify';
|
||||||
import type { MermaidConfig } from './config.type.js';
|
import type { MermaidConfig } from './config.type.js';
|
||||||
import { evaluate } from './diagrams/common/common.js';
|
import { evaluate } from './diagrams/common/common.js';
|
||||||
import isEmpty from 'lodash-es/isEmpty.js';
|
import isEmpty from 'lodash-es/isEmpty.js';
|
||||||
import { setA11yDiagramInfo, addSVGa11yTitleDescription } from './accessibility.js';
|
import { setA11yDiagramInfo, addSVGa11yTitleDescription } from './accessibility.js';
|
||||||
import { parseDirective } from './directiveUtils.js';
|
import { preprocessDiagram } from './preprocess.js';
|
||||||
import { extractFrontMatter } from './diagram-api/frontmatter.js';
|
|
||||||
|
|
||||||
// diagram names that support classDef statements
|
// diagram names that support classDef statements
|
||||||
const CLASSDEF_DIAGRAMS = [
|
const CLASSDEF_DIAGRAMS = [
|
||||||
@@ -98,6 +96,13 @@ export interface RenderResult {
|
|||||||
bindFunctions?: (element: Element) => void;
|
bindFunctions?: (element: Element) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function processAndSetConfigs(text: string) {
|
||||||
|
const processed = preprocessDiagram(text);
|
||||||
|
configApi.reset();
|
||||||
|
configApi.addDirective(processed.config);
|
||||||
|
return processed;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse the text and validate the syntax.
|
* Parse the text and validate the syntax.
|
||||||
* @param text - The mermaid diagram definition.
|
* @param text - The mermaid diagram definition.
|
||||||
@@ -108,6 +113,9 @@ export interface RenderResult {
|
|||||||
|
|
||||||
async function parse(text: string, parseOptions?: ParseOptions): Promise<boolean> {
|
async function parse(text: string, parseOptions?: ParseOptions): Promise<boolean> {
|
||||||
addDiagrams();
|
addDiagrams();
|
||||||
|
|
||||||
|
text = processAndSetConfigs(text).code;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await getDiagramFromText(text);
|
await getDiagramFromText(text);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -384,18 +392,8 @@ const render = async function (
|
|||||||
): Promise<RenderResult> {
|
): Promise<RenderResult> {
|
||||||
addDiagrams();
|
addDiagrams();
|
||||||
|
|
||||||
configApi.reset();
|
const processed = processAndSetConfigs(text);
|
||||||
|
text = processed.code;
|
||||||
// We need to add the directives before creating the diagram.
|
|
||||||
// So extractFrontMatter is called twice. Once here and once in the diagram parser.
|
|
||||||
// This can be fixed in a future refactor.
|
|
||||||
extractFrontMatter(text, {}, configApi.addDirective);
|
|
||||||
|
|
||||||
// Add Directives.
|
|
||||||
const graphInit = utils.detectInit(text);
|
|
||||||
if (graphInit) {
|
|
||||||
configApi.addDirective(graphInit);
|
|
||||||
}
|
|
||||||
|
|
||||||
const config = configApi.getConfig();
|
const config = configApi.getConfig();
|
||||||
log.debug(config);
|
log.debug(config);
|
||||||
@@ -405,15 +403,6 @@ const render = async function (
|
|||||||
text = MAX_TEXTLENGTH_EXCEEDED_MSG;
|
text = MAX_TEXTLENGTH_EXCEEDED_MSG;
|
||||||
}
|
}
|
||||||
|
|
||||||
// clean up text CRLFs
|
|
||||||
text = text.replace(/\r\n?/g, '\n'); // parser problems on CRLF ignore all CR and leave LF;;
|
|
||||||
|
|
||||||
// clean up html tags so that all attributes use single quotes, parser throws error on double quotes
|
|
||||||
text = text.replace(
|
|
||||||
/<(\w+)([^>]*)>/g,
|
|
||||||
(match, tag, attributes) => '<' + tag + attributes.replace(/="([^"]*)"/g, "='$1'") + '>'
|
|
||||||
);
|
|
||||||
|
|
||||||
const idSelector = '#' + id;
|
const idSelector = '#' + id;
|
||||||
const iFrameID = 'i' + id;
|
const iFrameID = 'i' + id;
|
||||||
const iFrameID_selector = '#' + iFrameID;
|
const iFrameID_selector = '#' + iFrameID;
|
||||||
@@ -476,7 +465,7 @@ const render = async function (
|
|||||||
let parseEncounteredException;
|
let parseEncounteredException;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
diag = await getDiagramFromText(text);
|
diag = await getDiagramFromText(text, { title: processed.title });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
diag = new Diagram('error');
|
diag = new Diagram('error');
|
||||||
parseEncounteredException = error;
|
parseEncounteredException = error;
|
||||||
@@ -673,7 +662,6 @@ function addA11yInfo(
|
|||||||
export const mermaidAPI = Object.freeze({
|
export const mermaidAPI = Object.freeze({
|
||||||
render,
|
render,
|
||||||
parse,
|
parse,
|
||||||
parseDirective,
|
|
||||||
getDiagramFromText,
|
getDiagramFromText,
|
||||||
initialize,
|
initialize,
|
||||||
getConfig: configApi.getConfig,
|
getConfig: configApi.getConfig,
|
||||||
|
58
packages/mermaid/src/preprocess.ts
Normal file
58
packages/mermaid/src/preprocess.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { cleanupComments } from './diagram-api/comments.js';
|
||||||
|
import { extractFrontMatter } from './diagram-api/frontmatter.js';
|
||||||
|
import utils, { cleanAndMerge, removeDirectives } from './utils.js';
|
||||||
|
|
||||||
|
const cleanupText = (code: string) => {
|
||||||
|
return (
|
||||||
|
code
|
||||||
|
// parser problems on CRLF ignore all CR and leave LF;;
|
||||||
|
.replace(/\r\n?/g, '\n')
|
||||||
|
// clean up html tags so that all attributes use single quotes, parser throws error on double quotes
|
||||||
|
.replace(
|
||||||
|
/<(\w+)([^>]*)>/g,
|
||||||
|
(match, tag, attributes) => '<' + tag + attributes.replace(/="([^"]*)"/g, "='$1'") + '>'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const processFrontmatter = (code: string) => {
|
||||||
|
const { text, metadata } = extractFrontMatter(code);
|
||||||
|
const { displayMode, title, config = {} } = metadata;
|
||||||
|
if (displayMode) {
|
||||||
|
// Needs to be supported for legacy reasons
|
||||||
|
if (!config.gantt) {
|
||||||
|
config.gantt = {};
|
||||||
|
}
|
||||||
|
config.gantt.displayMode = displayMode;
|
||||||
|
}
|
||||||
|
return { title, config, text };
|
||||||
|
};
|
||||||
|
|
||||||
|
const processDirectives = (code: string) => {
|
||||||
|
const initDirective = utils.detectInit(code) ?? {};
|
||||||
|
const wrapDirectives = utils.detectDirective(code, 'wrap');
|
||||||
|
if (Array.isArray(wrapDirectives)) {
|
||||||
|
initDirective.wrap = wrapDirectives.some(({ type }) => {
|
||||||
|
type === 'wrap';
|
||||||
|
});
|
||||||
|
} else if (wrapDirectives?.type === 'wrap') {
|
||||||
|
initDirective.wrap = true;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
text: removeDirectives(code),
|
||||||
|
directive: initDirective,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const preprocessDiagram = (code: string) => {
|
||||||
|
const cleanedCode = cleanupText(code);
|
||||||
|
const frontMatterResult = processFrontmatter(cleanedCode);
|
||||||
|
const directiveResult = processDirectives(frontMatterResult.text);
|
||||||
|
const config = cleanAndMerge(frontMatterResult.config, directiveResult.directive);
|
||||||
|
code = cleanupComments(directiveResult.text);
|
||||||
|
return {
|
||||||
|
code,
|
||||||
|
title: frontMatterResult.title,
|
||||||
|
config,
|
||||||
|
};
|
||||||
|
};
|
@@ -1,10 +1,11 @@
|
|||||||
import { vi } from 'vitest';
|
import { vi } from 'vitest';
|
||||||
import utils, { cleanAndMerge } from './utils.js';
|
import utils, { cleanAndMerge, detectDirective } from './utils.js';
|
||||||
import assignWithDepth from './assignWithDepth.js';
|
import assignWithDepth from './assignWithDepth.js';
|
||||||
import { detectType } from './diagram-api/detectType.js';
|
import { detectType } from './diagram-api/detectType.js';
|
||||||
import { addDiagrams } from './diagram-api/diagram-orchestration.js';
|
import { addDiagrams } from './diagram-api/diagram-orchestration.js';
|
||||||
import memoize from 'lodash-es/memoize.js';
|
import memoize from 'lodash-es/memoize.js';
|
||||||
import { MockedD3 } from './tests/MockedD3.js';
|
import { MockedD3 } from './tests/MockedD3.js';
|
||||||
|
import { preprocessDiagram } from './preprocess.js';
|
||||||
|
|
||||||
addDiagrams();
|
addDiagrams();
|
||||||
|
|
||||||
@@ -158,13 +159,38 @@ describe('when detecting chart type ', function () {
|
|||||||
const type = detectType(str);
|
const type = detectType(str);
|
||||||
expect(type).toBe('flowchart');
|
expect(type).toBe('flowchart');
|
||||||
});
|
});
|
||||||
|
it('should handle a wrap directive', () => {
|
||||||
|
const wrap = { type: 'wrap', args: null };
|
||||||
|
expect(detectDirective('%%{wrap}%%', 'wrap')).toEqual(wrap);
|
||||||
|
expect(
|
||||||
|
detectDirective(
|
||||||
|
`%%{
|
||||||
|
wrap
|
||||||
|
}%%`,
|
||||||
|
'wrap'
|
||||||
|
)
|
||||||
|
).toEqual(wrap);
|
||||||
|
expect(
|
||||||
|
detectDirective(
|
||||||
|
`%%{
|
||||||
|
|
||||||
|
wrap
|
||||||
|
|
||||||
|
}%%`,
|
||||||
|
'wrap'
|
||||||
|
)
|
||||||
|
).toEqual(wrap);
|
||||||
|
expect(detectDirective('%%{wrap:}%%', 'wrap')).toEqual(wrap);
|
||||||
|
expect(detectDirective('%%{wrap: }%%', 'wrap')).toEqual(wrap);
|
||||||
|
expect(detectDirective('graph', 'wrap')).not.toEqual(wrap);
|
||||||
|
});
|
||||||
it('should handle an initialize definition', function () {
|
it('should handle an initialize definition', function () {
|
||||||
const str = `
|
const str = `
|
||||||
%%{initialize: { 'logLevel': 0, 'theme': 'dark' }}%%
|
%%{initialize: { 'logLevel': 0, 'theme': 'dark' }}%%
|
||||||
sequenceDiagram
|
sequenceDiagram
|
||||||
Alice->Bob: hi`;
|
Alice->Bob: hi`;
|
||||||
const type = detectType(str);
|
const type = detectType(str);
|
||||||
const init = utils.detectInit(str);
|
const init = preprocessDiagram(str).config;
|
||||||
expect(type).toBe('sequence');
|
expect(type).toBe('sequence');
|
||||||
expect(init).toEqual({ logLevel: 0, theme: 'dark' });
|
expect(init).toEqual({ logLevel: 0, theme: 'dark' });
|
||||||
});
|
});
|
||||||
@@ -174,7 +200,7 @@ Alice->Bob: hi`;
|
|||||||
sequenceDiagram
|
sequenceDiagram
|
||||||
Alice->Bob: hi`;
|
Alice->Bob: hi`;
|
||||||
const type = detectType(str);
|
const type = detectType(str);
|
||||||
const init = utils.detectInit(str);
|
const init = preprocessDiagram(str).config;
|
||||||
expect(type).toBe('sequence');
|
expect(type).toBe('sequence');
|
||||||
expect(init).toEqual({ logLevel: 0, theme: 'dark' });
|
expect(init).toEqual({ logLevel: 0, theme: 'dark' });
|
||||||
});
|
});
|
||||||
@@ -184,7 +210,7 @@ Alice->Bob: hi`;
|
|||||||
sequenceDiagram
|
sequenceDiagram
|
||||||
Alice->Bob: hi`;
|
Alice->Bob: hi`;
|
||||||
const type = detectType(str);
|
const type = detectType(str);
|
||||||
const init = utils.detectInit(str);
|
const init = preprocessDiagram(str).config;
|
||||||
expect(type).toBe('sequence');
|
expect(type).toBe('sequence');
|
||||||
expect(init).toEqual({ logLevel: 0, theme: 'dark', sequence: { wrap: true } });
|
expect(init).toEqual({ logLevel: 0, theme: 'dark', sequence: { wrap: true } });
|
||||||
});
|
});
|
||||||
@@ -199,7 +225,7 @@ Alice->Bob: hi`;
|
|||||||
sequenceDiagram
|
sequenceDiagram
|
||||||
Alice->Bob: hi`;
|
Alice->Bob: hi`;
|
||||||
const type = detectType(str);
|
const type = detectType(str);
|
||||||
const init = utils.detectInit(str);
|
const init = preprocessDiagram(str).config;
|
||||||
expect(type).toBe('sequence');
|
expect(type).toBe('sequence');
|
||||||
expect(init).toEqual({ logLevel: 0, theme: 'dark' });
|
expect(init).toEqual({ logLevel: 0, theme: 'dark' });
|
||||||
});
|
});
|
||||||
@@ -214,7 +240,7 @@ Alice->Bob: hi`;
|
|||||||
sequenceDiagram
|
sequenceDiagram
|
||||||
Alice->Bob: hi`;
|
Alice->Bob: hi`;
|
||||||
const type = detectType(str);
|
const type = detectType(str);
|
||||||
const init = utils.detectInit(str);
|
const init = preprocessDiagram(str).config;
|
||||||
expect(type).toBe('sequence');
|
expect(type).toBe('sequence');
|
||||||
expect(init).toEqual({ logLevel: 0, theme: 'dark' });
|
expect(init).toEqual({ logLevel: 0, theme: 'dark' });
|
||||||
});
|
});
|
||||||
|
@@ -25,7 +25,7 @@ import {
|
|||||||
select,
|
select,
|
||||||
} from 'd3';
|
} from 'd3';
|
||||||
import common from './diagrams/common/common.js';
|
import common from './diagrams/common/common.js';
|
||||||
import { configKeys } from './defaultConfig.js';
|
import { sanitizeDirective } from './utils/sanitizeDirective.js';
|
||||||
import { log } from './logger.js';
|
import { log } from './logger.js';
|
||||||
import { detectType } from './diagram-api/detectType.js';
|
import { detectType } from './diagram-api/detectType.js';
|
||||||
import assignWithDepth from './assignWithDepth.js';
|
import assignWithDepth from './assignWithDepth.js';
|
||||||
@@ -62,7 +62,6 @@ const d3CurveTypes = {
|
|||||||
|
|
||||||
const directiveWithoutOpen =
|
const directiveWithoutOpen =
|
||||||
/\s*(?:(\w+)(?=:):|(\w+))\s*(?:(\w+)|((?:(?!}%{2}).|\r?\n)*))?\s*(?:}%{2})?/gi;
|
/\s*(?:(\w+)(?=:):|(\w+))\s*(?:(\w+)|((?:(?!}%{2}).|\r?\n)*))?\s*(?:}%{2})?/gi;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Detects the init config object from the text
|
* Detects the init config object from the text
|
||||||
*
|
*
|
||||||
@@ -197,6 +196,10 @@ export const detectDirective = function (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const removeDirectives = function (text: string): string {
|
||||||
|
return text.replace(directiveRegex, '');
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Detects whether a substring in present in a given array
|
* Detects whether a substring in present in a given array
|
||||||
*
|
*
|
||||||
@@ -842,88 +845,6 @@ export const entityDecode = function (html: string): string {
|
|||||||
return unescape(decoder.textContent);
|
return unescape(decoder.textContent);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Sanitizes directive objects
|
|
||||||
*
|
|
||||||
* @param args - Directive's JSON
|
|
||||||
*/
|
|
||||||
export const sanitizeDirective = (args: unknown): void => {
|
|
||||||
log.debug('sanitizeDirective called with', args);
|
|
||||||
|
|
||||||
// Return if not an object
|
|
||||||
if (typeof args !== 'object' || args == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sanitize each element if an array
|
|
||||||
if (Array.isArray(args)) {
|
|
||||||
args.forEach((arg) => sanitizeDirective(arg));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sanitize each key if an object
|
|
||||||
for (const key of Object.keys(args)) {
|
|
||||||
log.debug('Checking key', key);
|
|
||||||
if (
|
|
||||||
key.startsWith('__') ||
|
|
||||||
key.includes('proto') ||
|
|
||||||
key.includes('constr') ||
|
|
||||||
!configKeys.has(key) ||
|
|
||||||
args[key] == null
|
|
||||||
) {
|
|
||||||
log.debug('sanitize deleting key: ', key);
|
|
||||||
delete args[key];
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Recurse if an object
|
|
||||||
if (typeof args[key] === 'object') {
|
|
||||||
log.debug('sanitizing object', key);
|
|
||||||
sanitizeDirective(args[key]);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const cssMatchers = ['themeCSS', 'fontFamily', 'altFontFamily'];
|
|
||||||
for (const cssKey of cssMatchers) {
|
|
||||||
if (key.includes(cssKey)) {
|
|
||||||
log.debug('sanitizing css option', key);
|
|
||||||
args[key] = sanitizeCss(args[key]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (args.themeVariables) {
|
|
||||||
for (const k of Object.keys(args.themeVariables)) {
|
|
||||||
const val = args.themeVariables[k];
|
|
||||||
if (val?.match && !val.match(/^[\d "#%(),.;A-Za-z]+$/)) {
|
|
||||||
args.themeVariables[k] = '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
log.debug('After sanitization', args);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const sanitizeCss = (str: string): string => {
|
|
||||||
let startCnt = 0;
|
|
||||||
let endCnt = 0;
|
|
||||||
|
|
||||||
for (const element of str) {
|
|
||||||
if (startCnt < endCnt) {
|
|
||||||
return '{ /* ERROR: Unbalanced CSS */ }';
|
|
||||||
}
|
|
||||||
if (element === '{') {
|
|
||||||
startCnt++;
|
|
||||||
} else if (element === '}') {
|
|
||||||
endCnt++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (startCnt !== endCnt) {
|
|
||||||
return '{ /* ERROR: Unbalanced CSS */ }';
|
|
||||||
}
|
|
||||||
// Todo add more checks here
|
|
||||||
return str;
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface DetailedError {
|
export interface DetailedError {
|
||||||
str: string;
|
str: string;
|
||||||
hash: any;
|
hash: any;
|
||||||
@@ -1021,8 +942,6 @@ export default {
|
|||||||
runFunc,
|
runFunc,
|
||||||
entityDecode,
|
entityDecode,
|
||||||
initIdGenerator,
|
initIdGenerator,
|
||||||
sanitizeDirective,
|
|
||||||
sanitizeCss,
|
|
||||||
insertTitle,
|
insertTitle,
|
||||||
parseFontSize,
|
parseFontSize,
|
||||||
};
|
};
|
||||||
|
84
packages/mermaid/src/utils/sanitizeDirective.ts
Normal file
84
packages/mermaid/src/utils/sanitizeDirective.ts
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import { configKeys } from '../defaultConfig.js';
|
||||||
|
import { log } from '../logger.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sanitizes directive objects
|
||||||
|
*
|
||||||
|
* @param args - Directive's JSON
|
||||||
|
*/
|
||||||
|
export const sanitizeDirective = (args: any): void => {
|
||||||
|
log.debug('sanitizeDirective called with', args);
|
||||||
|
|
||||||
|
// Return if not an object
|
||||||
|
if (typeof args !== 'object' || args == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanitize each element if an array
|
||||||
|
if (Array.isArray(args)) {
|
||||||
|
args.forEach((arg) => sanitizeDirective(arg));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanitize each key if an object
|
||||||
|
for (const key of Object.keys(args)) {
|
||||||
|
log.debug('Checking key', key);
|
||||||
|
if (
|
||||||
|
key.startsWith('__') ||
|
||||||
|
key.includes('proto') ||
|
||||||
|
key.includes('constr') ||
|
||||||
|
!configKeys.has(key) ||
|
||||||
|
args[key] == null
|
||||||
|
) {
|
||||||
|
log.debug('sanitize deleting key: ', key);
|
||||||
|
delete args[key];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recurse if an object
|
||||||
|
if (typeof args[key] === 'object') {
|
||||||
|
log.debug('sanitizing object', key);
|
||||||
|
sanitizeDirective(args[key]);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cssMatchers = ['themeCSS', 'fontFamily', 'altFontFamily'];
|
||||||
|
for (const cssKey of cssMatchers) {
|
||||||
|
if (key.includes(cssKey)) {
|
||||||
|
log.debug('sanitizing css option', key);
|
||||||
|
args[key] = sanitizeCss(args[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.themeVariables) {
|
||||||
|
for (const k of Object.keys(args.themeVariables)) {
|
||||||
|
const val = args.themeVariables[k];
|
||||||
|
if (val?.match && !val.match(/^[\d "#%(),.;A-Za-z]+$/)) {
|
||||||
|
args.themeVariables[k] = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.debug('After sanitization', args);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const sanitizeCss = (str: string): string => {
|
||||||
|
let startCnt = 0;
|
||||||
|
let endCnt = 0;
|
||||||
|
|
||||||
|
for (const element of str) {
|
||||||
|
if (startCnt < endCnt) {
|
||||||
|
return '{ /* ERROR: Unbalanced CSS */ }';
|
||||||
|
}
|
||||||
|
if (element === '{') {
|
||||||
|
startCnt++;
|
||||||
|
} else if (element === '}') {
|
||||||
|
endCnt++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (startCnt !== endCnt) {
|
||||||
|
return '{ /* ERROR: Unbalanced CSS */ }';
|
||||||
|
}
|
||||||
|
// Todo add more checks here
|
||||||
|
return str;
|
||||||
|
};
|
Reference in New Issue
Block a user