Merge branch 'develop' into next

* develop:
  Remove unnecessary tests
  Remove optional chaining
  refactor: Use `||` instead of `??`
  core: Adapt changes from 3f7bafb2d7
  Update cypress/helpers/util.js
  chore: Add deprecation notices, improve types
  chore: Cleanup gitGraph tests
  Update README.md
  refactor: Move setWrap to individual diagrams as necessary.
  refactor: Remove directives from grammar
  refactor: Update DBs to remove directive handling
  refactor: Move directive processing before parsing
  I refactored the code to improve its time complexity by removing unnecessary code and optimizing the existing code.
This commit is contained in:
Sidharth Vinod 2023-09-08 16:36:45 +05:30
commit 997c23befa
No known key found for this signature in database
GPG Key ID: FB5CCD378D3907CD
56 changed files with 494 additions and 1139 deletions

View File

@ -165,13 +165,7 @@ class Class10 {
int id int id
size() size()
} }
namespace Namespace01 {
class Class11
class Class12 {
int id
size()
}
}
``` ```
```mermaid ```mermaid
@ -191,13 +185,7 @@ class Class10 {
int id int id
size() size()
} }
namespace Namespace01 {
class Class11
class Class12 {
int id
size()
}
}
``` ```
### State diagram [<a href="https://mermaid-js.github.io/mermaid/#/stateDiagram">docs</a> - <a href="https://mermaid.live/edit#pako:eNpdkEFvgzAMhf8K8nEqpYSNthx22Xbcqcexg0sCiZQQlDhIFeK_L8A6TfXp6fOz9ewJGssFVOAJSbwr7ByadGR1n8T6evpO0vQ1uZDSekOrXGFsPqJPO6q-2-imH8f_0TeHXm50lfelsAMjnEHFY6xpMdRAUhhRQxUlFy0GTTXU_RytYeAx-AdXZB1ULWovdoCB7OXWN1CRC-Ju-r3uz6UtchGHJqDbsPygU57iysb2reoWHpyOWBINvsqypb3vFMlw3TfWZF5xiY7keC6zkpUnZIUojwW-FAVvrvn51LLnvOXHQ84Q5nn-AVtLcwk">live editor</a>] ### State diagram [<a href="https://mermaid-js.github.io/mermaid/#/stateDiagram">docs</a> - <a href="https://mermaid.live/edit#pako:eNpdkEFvgzAMhf8K8nEqpYSNthx22Xbcqcexg0sCiZQQlDhIFeK_L8A6TfXp6fOz9ewJGssFVOAJSbwr7ByadGR1n8T6evpO0vQ1uZDSekOrXGFsPqJPO6q-2-imH8f_0TeHXm50lfelsAMjnEHFY6xpMdRAUhhRQxUlFy0GTTXU_RytYeAx-AdXZB1ULWovdoCB7OXWN1CRC-Ju-r3uz6UtchGHJqDbsPygU57iysb2reoWHpyOWBINvsqypb3vFMlw3TfWZF5xiY7keC6zkpUnZIUojwW-FAVvrvn51LLnvOXHQ84Q5nn-AVtLcwk">live editor</a>]

View File

@ -52,29 +52,21 @@ export const imgSnapshotTest = (
api = false, api = false,
validation?: any validation?: any
): void => { ): void => {
cy.log(JSON.stringify(_options)); const options: CypressMermaidConfig = {
const options: CypressMermaidConfig = Object.assign(_options); ..._options,
if (!options.fontFamily) { fontFamily: _options.fontFamily || 'courier',
options.fontFamily = 'courier'; // @ts-ignore TODO: Fix type of fontSize
} fontSize: _options.fontSize || '16px',
if (!options.sequence) { sequence: {
options.sequence = {}; ...(_options.sequence || {}),
} actorFontFamily: 'courier',
if (!options.sequence || (options.sequence && !options.sequence.actorFontFamily)) { noteFontFamily:
options.sequence.actorFontFamily = 'courier'; _options.sequence && _options.sequence.noteFontFamily
} ? _options.sequence.noteFontFamily
if (options.sequence && !options.sequence.noteFontFamily) { : 'courier',
options.sequence.noteFontFamily = 'courier'; messageFontFamily: 'courier',
} },
options.sequence.actorFontFamily = 'courier'; };
options.sequence.noteFontFamily = 'courier';
options.sequence.messageFontFamily = 'courier';
if (options.sequence && !options.sequence.actorFontFamily) {
options.sequence.actorFontFamily = 'courier';
}
if (!options.fontSize) {
options.fontSize = 16;
}
const url: string = mermaidUrl(graphStr, options, api); const url: string = mermaidUrl(graphStr, options, api);
openURLAndVerifyRendering(url, options, validation); openURLAndVerifyRendering(url, options, validation);
@ -82,11 +74,10 @@ export const imgSnapshotTest = (
export const urlSnapshotTest = ( export const urlSnapshotTest = (
url: string, url: string,
_options: CypressMermaidConfig, options: CypressMermaidConfig,
_api = false, _api = false,
validation?: any validation?: any
): void => { ): void => {
const options: CypressMermaidConfig = Object.assign(_options);
openURLAndVerifyRendering(url, options, validation); openURLAndVerifyRendering(url, options, validation);
}; };

View File

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

View File

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

View File

@ -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`: `Pick`<`DiagramMetadata`, `"title"`>) => `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)

View File

@ -2,11 +2,9 @@ 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, DiagramMetadata } from './diagram-api/types.js';
export type ParseErrorFunction = (err: string | DetailedError | unknown, hash?: any) => void; export type ParseErrorFunction = (err: string | DetailedError | unknown, hash?: any) => void;
@ -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: Pick<DiagramMetadata, 'title'> = {}) {
this.text += '\n'; this.text += '\n';
const cnf = configApi.getConfig(); const cnf = configApi.getConfig();
try { try {
@ -37,20 +35,7 @@ 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); if (this.parser.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)));
if (this.parser.parser !== undefined) {
// The parser.parser.yy is only present in JISON parsers. So, we'll only set if required. // The parser.parser.yy is only present in JISON parsers. So, we'll only set if required.
this.parser.parser.yy = this.db; this.parser.parser.yy = this.db;
} }
@ -63,7 +48,12 @@ 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);
// 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);
}
this.parser.parse(this.text); this.parser.parse(this.text);
} }
@ -85,11 +75,15 @@ export class Diagram {
* **Warning:** This function may be changed in the future. * **Warning:** This function may be changed in the future.
* @alpha * @alpha
* @param text - The mermaid diagram definition. * @param text - The mermaid diagram definition.
* @param metadata - Diagram metadata, defined in YAML.
* @returns A the Promise of a Diagram object. * @returns A the Promise of a Diagram object.
* @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: Pick<DiagramMetadata, 'title'> = {}
): 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
@ -104,5 +98,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);
}; };

View File

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

View File

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

View File

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

View File

@ -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 '../diagrams/common/commonDb.js'; import * as _commonDb from '../diagrams/common/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,18 @@ 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 in https://github.com/mermaid-js/mermaid/pull/4759.
); // This is a no-op for legacy support.
} }
);
}; };
export const getDiagram = (name: string): DiagramDefinition => { export const getDiagram = (name: string): DiagramDefinition => {

View File

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

View File

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

View File

@ -1,7 +1,13 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { Diagram } from '../Diagram.js'; import type { Diagram } from '../Diagram.js';
import type { BaseDiagramConfig, MermaidConfig } from '../config.type.js'; import type { BaseDiagramConfig, MermaidConfig } from '../config.type.js';
import type * as d3 from 'd3'; import type * as d3 from 'd3';
export interface DiagramMetadata {
title?: string;
config?: MermaidConfig;
}
export interface InjectUtils { export interface InjectUtils {
_log: any; _log: any;
_setLogLevel: any; _setLogLevel: any;
@ -9,6 +15,7 @@ export interface InjectUtils {
_sanitizeText: any; _sanitizeText: any;
_setupGraphViewbox: any; _setupGraphViewbox: any;
_commonDb: any; _commonDb: any;
/** @deprecated as directives will be pre-processed since https://github.com/mermaid-js/mermaid/pull/4759 */
_parseDirective: any; _parseDirective: any;
} }
@ -45,6 +52,7 @@ export interface DiagramDefinition {
_sanitizeText: InjectUtils['_sanitizeText'], _sanitizeText: InjectUtils['_sanitizeText'],
_setupGraphViewbox: InjectUtils['_setupGraphViewbox'], _setupGraphViewbox: InjectUtils['_setupGraphViewbox'],
_commonDb: InjectUtils['_commonDb'], _commonDb: InjectUtils['_commonDb'],
/** @deprecated as directives will be pre-processed since https://github.com/mermaid-js/mermaid/pull/4759 */
_parseDirective: InjectUtils['_parseDirective'] _parseDirective: InjectUtils['_parseDirective']
) => void; ) => void;
} }
@ -83,15 +91,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>;

View File

@ -1,4 +1,3 @@
import mermaidAPI from '../../mermaidAPI.js';
import * as configApi from '../../config.js'; import * as configApi from '../../config.js';
import { sanitizeText } from '../common/common.js'; import { sanitizeText } from '../common/common.js';
import { import {
@ -38,10 +37,6 @@ export const setC4Type = function (c4TypeParam) {
c4Type = sanitizedText; c4Type = sanitizedText;
}; };
export const parseDirective = function (statement, context, type) {
mermaidAPI.parseDirective(this, statement, context, type);
};
//type, from, to, label, ?techn, ?descr, ?sprite, ?tags, $link //type, from, to, label, ?techn, ?descr, ?sprite, ?tags, $link
export const addRel = function (type, from, to, label, techn, descr, sprite, tags, link) { export const addRel = function (type, from, to, label, techn, descr, sprite, tags, link) {
// Don't allow label nulling // Don't allow label nulling
@ -821,7 +816,6 @@ export default {
getAccTitle, getAccTitle,
getAccDescription, getAccDescription,
setAccDescription, setAccDescription,
parseDirective,
getConfig: () => configApi.getConfig().c4, getConfig: () => configApi.getConfig().c4,
clear, clear,
LINETYPE, LINETYPE,

View File

@ -1,17 +1,18 @@
// @ts-ignore: JISON doesn't support types // @ts-ignore: JISON doesn't support types
import c4Parser from './parser/c4Diagram.jison'; import parser from './parser/c4Diagram.jison';
import c4Db from './c4Db.js'; import db from './c4Db.js';
import c4Renderer from './c4Renderer.js'; import renderer from './c4Renderer.js';
import c4Styles from './styles.js'; import styles from './styles.js';
import type { MermaidConfig } from '../../config.type.js'; import type { MermaidConfig } from '../../config.type.js';
import type { DiagramDefinition } from '../../diagram-api/types.js'; import type { DiagramDefinition } from '../../diagram-api/types.js';
export const diagram: DiagramDefinition = { export const diagram: DiagramDefinition = {
parser: c4Parser, parser,
db: c4Db, db,
renderer: c4Renderer, renderer,
styles: c4Styles, styles,
init: (cnf: MermaidConfig) => { init: ({ c4, wrap }: MermaidConfig) => {
c4Renderer.setConf(cnf.c4); renderer.setConf(c4);
db.setWrap(wrap);
}, },
}; };

View File

@ -72,25 +72,16 @@
%x string_kv_key %x string_kv_key
%x string_kv_value %x string_kv_value
%x open_directive
%x type_directive
%x arg_directive
%x close_directive
%x acc_title %x acc_title
%x acc_descr %x acc_descr
%x acc_descr_multiline %x acc_descr_multiline
%% %%
\%\%\{ { this.begin('open_directive'); return 'open_directive'; }
.*direction\s+TB[^\n]* return 'direction_tb'; .*direction\s+TB[^\n]* return 'direction_tb';
.*direction\s+BT[^\n]* return 'direction_bt'; .*direction\s+BT[^\n]* return 'direction_bt';
.*direction\s+RL[^\n]* return 'direction_rl'; .*direction\s+RL[^\n]* return 'direction_rl';
.*direction\s+LR[^\n]* return 'direction_lr'; .*direction\s+LR[^\n]* return 'direction_lr';
<open_directive>((?:(?!\}\%\%)[^:.])*) { this.begin('type_directive'); return 'type_directive'; }
<type_directive>":" { this.popState(); this.begin('arg_directive'); return ':'; }
<type_directive,arg_directive>\}\%\% { this.popState(); this.popState(); return 'close_directive'; }
<arg_directive>((?:(?!\}\%\%).|\n)*) return 'arg_directive';
"title"\s[^#\n;]+ return 'title'; "title"\s[^#\n;]+ return 'title';
@ -207,7 +198,6 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multiline");}
start start
: mermaidDoc : mermaidDoc
| direction | direction
| directive start
; ;
direction direction
@ -225,26 +215,6 @@ mermaidDoc
: graphConfig : graphConfig
; ;
directive
: openDirective typeDirective closeDirective NEWLINE
| openDirective typeDirective ':' argDirective closeDirective NEWLINE
;
openDirective
: open_directive { yy.parseDirective('%%{', 'open_directive'); }
;
typeDirective
: type_directive { }
;
argDirective
: arg_directive { $1 = $1.trim().replace(/'/g, '"'); yy.parseDirective($1, 'arg_directive'); }
;
closeDirective
: close_directive { yy.parseDirective('}%%', 'close_directive', 'c4Context'); }
;
graphConfig graphConfig
: C4_CONTEXT NEWLINE statements EOF {yy.setC4Type($1)} : C4_CONTEXT NEWLINE statements EOF {yy.setC4Type($1)}

View File

@ -4,7 +4,6 @@ import { log } from '../../logger.js';
import * as configApi from '../../config.js'; import * as configApi from '../../config.js';
import common from '../common/common.js'; import common from '../common/common.js';
import utils from '../../utils.js'; import utils from '../../utils.js';
import mermaidAPI from '../../mermaidAPI.js';
import { import {
setAccTitle, setAccTitle,
getAccTitle, getAccTitle,
@ -37,11 +36,6 @@ let functions: any[] = [];
const sanitizeText = (txt: string) => common.sanitizeText(txt, configApi.getConfig()); const sanitizeText = (txt: string) => common.sanitizeText(txt, configApi.getConfig());
export const parseDirective = function (statement: string, context: string, type: string) {
// @ts-ignore Don't wanna mess it up
mermaidAPI.parseDirective(this, statement, context, type);
};
const splitClassNameAndType = function (id: string) { const splitClassNameAndType = function (id: string) {
let genericType = ''; let genericType = '';
let className = id; let className = id;
@ -460,7 +454,6 @@ export const addClassesToNamespace = function (id: string, classNames: string[])
}; };
export default { export default {
parseDirective,
setAccTitle, setAccTitle,
getAccTitle, getAccTitle,
getAccDescription, getAccDescription,

View File

@ -13,9 +13,6 @@
%x href %x href
%x callback_name %x callback_name
%x callback_args %x callback_args
%x open_directive
%x type_directive
%x arg_directive
%x acc_title %x acc_title
%x acc_descr %x acc_descr
%x acc_descr_multiline %x acc_descr_multiline
@ -24,15 +21,10 @@
%x namespace %x namespace
%x namespace-body %x namespace-body
%% %%
\%\%\{ { this.begin('open_directive'); return 'open_directive'; }
.*direction\s+TB[^\n]* return 'direction_tb'; .*direction\s+TB[^\n]* return 'direction_tb';
.*direction\s+BT[^\n]* return 'direction_bt'; .*direction\s+BT[^\n]* return 'direction_bt';
.*direction\s+RL[^\n]* return 'direction_rl'; .*direction\s+RL[^\n]* return 'direction_rl';
.*direction\s+LR[^\n]* return 'direction_lr'; .*direction\s+LR[^\n]* return 'direction_lr';
<open_directive>((?:(?!\}\%\%)[^:.])*) { this.begin('type_directive'); return 'type_directive'; }
<type_directive>":" { this.popState(); this.begin('arg_directive'); return ':'; }
<type_directive,arg_directive>\}\%\% { this.popState(); this.popState(); return 'close_directive'; }
<arg_directive>((?:(?!\}\%\%).|\n)*) return 'arg_directive';
\%\%(?!\{)*[^\n]*(\r?\n?)+ /* skip comments */ \%\%(?!\{)*[^\n]*(\r?\n?)+ /* skip comments */
\%\%[^\n]*(\r?\n)* /* skip comments */ \%\%[^\n]*(\r?\n)* /* skip comments */
accTitle\s*":"\s* { this.begin("acc_title");return 'acc_title'; } accTitle\s*":"\s* { this.begin("acc_title");return 'acc_title'; }
@ -220,7 +212,6 @@ line was introduced with 'click'.
start start
: mermaidDoc : mermaidDoc
| directive start
| statements | statements
; ;
@ -232,27 +223,6 @@ graphConfig
: CLASS_DIAGRAM NEWLINE statements EOF : CLASS_DIAGRAM NEWLINE statements EOF
; ;
directive
: openDirective typeDirective closeDirective NEWLINE
| openDirective typeDirective ':' argDirective closeDirective NEWLINE
;
openDirective
: open_directive { yy.parseDirective('%%{', 'open_directive'); }
;
typeDirective
: type_directive { yy.parseDirective($1, 'type_directive'); }
;
argDirective
: arg_directive { $1 = $1.trim().replace(/'/g, '"'); yy.parseDirective($1, 'arg_directive'); }
;
closeDirective
: close_directive { yy.parseDirective('}%%', 'close_directive', 'class'); }
;
statements statements
: statement : statement
| statement NEWLINE | statement NEWLINE

View File

@ -1,5 +1,4 @@
import { log } from '../../logger.js'; import { log } from '../../logger.js';
import mermaidAPI from '../../mermaidAPI.js';
import * as configApi from '../../config.js'; import * as configApi from '../../config.js';
import { import {
@ -28,10 +27,6 @@ const Identification = {
IDENTIFYING: 'IDENTIFYING', IDENTIFYING: 'IDENTIFYING',
}; };
export const parseDirective = function (statement, context, type) {
mermaidAPI.parseDirective(this, statement, context, type);
};
const addEntity = function (name, alias = undefined) { const addEntity = function (name, alias = undefined) {
if (entities[name] === undefined) { if (entities[name] === undefined) {
entities[name] = { attributes: [], alias: alias }; entities[name] = { attributes: [], alias: alias };
@ -88,7 +83,6 @@ const clear = function () {
export default { export default {
Cardinality, Cardinality,
Identification, Identification,
parseDirective,
getConfig: () => configApi.getConfig().er, getConfig: () => configApi.getConfig().er,
addEntity, addEntity,
addAttributes, addAttributes,

View File

@ -1,7 +1,7 @@
%lex %lex
%options case-insensitive %options case-insensitive
%x open_directive type_directive arg_directive block %x block
%x acc_title %x acc_title
%x acc_descr %x acc_descr
%x acc_descr_multiline %x acc_descr_multiline
@ -14,11 +14,6 @@ accDescr\s*":"\s* { this.begin("ac
accDescr\s*"{"\s* { this.begin("acc_descr_multiline");} accDescr\s*"{"\s* { this.begin("acc_descr_multiline");}
<acc_descr_multiline>[\}] { this.popState(); } <acc_descr_multiline>[\}] { this.popState(); }
<acc_descr_multiline>[^\}]* return "acc_descr_multiline_value"; <acc_descr_multiline>[^\}]* return "acc_descr_multiline_value";
\%\%\{ { this.begin('open_directive'); return 'open_directive'; }
<open_directive>((?:(?!\}\%\%)[^:.])*) { this.begin('type_directive'); return 'type_directive'; }
<type_directive>":" { this.popState(); this.begin('arg_directive'); return ':'; }
<type_directive,arg_directive>\}\%\% { this.popState(); this.popState(); return 'close_directive'; }
<arg_directive>((?:(?!\}\%\%).|\n)*) return 'arg_directive';
[\n]+ return 'NEWLINE'; [\n]+ return 'NEWLINE';
\s+ /* skip whitespace */ \s+ /* skip whitespace */
[\s]+ return 'SPACE'; [\s]+ return 'SPACE';
@ -77,7 +72,6 @@ o\{ return 'ZERO_OR_MORE';
start start
: 'ER_DIAGRAM' document 'EOF' { /*console.log('finished parsing');*/ } : 'ER_DIAGRAM' document 'EOF' { /*console.log('finished parsing');*/ }
| directive start
; ;
document document
@ -92,14 +86,9 @@ line
| EOF { $$=[];} | EOF { $$=[];}
; ;
directive
: openDirective typeDirective closeDirective 'NEWLINE'
| openDirective typeDirective ':' argDirective closeDirective 'NEWLINE'
;
statement statement
: directive : entityName relSpec entityName ':' role
| entityName relSpec entityName ':' role
{ {
yy.addEntity($1); yy.addEntity($1);
yy.addEntity($3); yy.addEntity($3);
@ -191,20 +180,4 @@ role
| 'ALPHANUM' { $$ = $1; } | 'ALPHANUM' { $$ = $1; }
; ;
openDirective
: open_directive { yy.parseDirective('%%{', 'open_directive'); }
;
typeDirective
: type_directive { yy.parseDirective($1, 'type_directive'); }
;
argDirective
: arg_directive { $1 = $1.trim().replace(/'/g, '"'); yy.parseDirective($1, 'arg_directive'); }
;
closeDirective
: close_directive { yy.parseDirective('}%%', 'close_directive', 'er'); }
;
%% %%

View File

@ -2,7 +2,6 @@ import { select } from 'd3';
import utils from '../../utils.js'; import utils from '../../utils.js';
import * as configApi from '../../config.js'; import * as configApi from '../../config.js';
import common from '../common/common.js'; import common from '../common/common.js';
import mermaidAPI from '../../mermaidAPI.js';
import { log } from '../../logger.js'; import { log } from '../../logger.js';
import { import {
setAccTitle, setAccTitle,
@ -34,10 +33,6 @@ let funs = [];
const sanitizeText = (txt) => common.sanitizeText(txt, config); const sanitizeText = (txt) => common.sanitizeText(txt, config);
export const parseDirective = function (statement, context, type) {
mermaidAPI.parseDirective(this, statement, context, type);
};
/** /**
* Function to lookup domId from id in the graph definition. * Function to lookup domId from id in the graph definition.
* *
@ -771,7 +766,6 @@ export const lex = {
firstGraph, firstGraph,
}; };
export default { export default {
parseDirective,
defaultConfig: () => configApi.defaultConfig.flowchart, defaultConfig: () => configApi.defaultConfig.flowchart,
setAccTitle, setAccTitle,
getAccTitle, getAccTitle,

View File

@ -23,17 +23,8 @@
%x href %x href
%x callbackname %x callbackname
%x callbackargs %x callbackargs
%x open_directive
%x type_directive
%x arg_directive
%x close_directive
%% %%
\%\%\{ { this.begin('open_directive'); return 'open_directive'; }
<open_directive>((?:(?!\}\%\%)[^:.])*) { this.begin('type_directive'); return 'type_directive'; }
<type_directive>":" { this.popState(); this.begin('arg_directive'); return ':'; }
<type_directive,arg_directive>\}\%\% { this.popState(); this.popState(); return 'close_directive'; }
<arg_directive>((?:(?!\}\%\%).|\n)*) return 'arg_directive';
accTitle\s*":"\s* { this.begin("acc_title");return 'acc_title'; } accTitle\s*":"\s* { this.begin("acc_title");return 'acc_title'; }
<acc_title>(?!\n|;|#)*[^\n]* { this.popState(); return "acc_title_value"; } <acc_title>(?!\n|;|#)*[^\n]* { this.popState(); return "acc_title_value"; }
accDescr\s*":"\s* { this.begin("acc_descr");return 'acc_descr'; } accDescr\s*":"\s* { this.begin("acc_descr");return 'acc_descr'; }
@ -272,35 +263,10 @@ that id.
%% /* language grammar */ %% /* language grammar */
start start
: mermaidDoc
| directive start
;
directive
: openDirective typeDirective closeDirective separator
| openDirective typeDirective ':' argDirective closeDirective separator
;
openDirective
: open_directive { yy.parseDirective('%%{', 'open_directive'); }
;
typeDirective
: type_directive { yy.parseDirective($type_directive, 'type_directive'); }
;
argDirective
: arg_directive { $arg_directive = $arg_directive.trim().replace(/'/g, '"'); yy.parseDirective($arg_directive, 'arg_directive'); }
;
closeDirective
: close_directive { yy.parseDirective('}%%', 'close_directive', 'flowchart'); }
;
mermaidDoc
: graphConfig document : graphConfig document
; ;
document document
: /* empty */ : /* empty */
{ $$ = [];} { $$ = [];}

View File

@ -6,7 +6,6 @@ import dayjsAdvancedFormat from 'dayjs/plugin/advancedFormat.js';
import { log } from '../../logger.js'; import { log } from '../../logger.js';
import * as configApi from '../../config.js'; import * as configApi from '../../config.js';
import utils from '../../utils.js'; import utils from '../../utils.js';
import mermaidAPI from '../../mermaidAPI.js';
import { import {
setAccTitle, setAccTitle,
@ -42,10 +41,6 @@ let weekday = 'sunday';
// The serial order of the task in the script // The serial order of the task in the script
let lastOrder = 0; let lastOrder = 0;
export const parseDirective = function (statement, context, type) {
mermaidAPI.parseDirective(this, statement, context, type);
};
export const clear = function () { export const clear = function () {
sections = []; sections = [];
tasks = []; tasks = [];
@ -730,7 +725,6 @@ export const bindFunctions = function (element) {
}; };
export default { export default {
parseDirective,
getConfig: () => configApi.getConfig().gantt, getConfig: () => configApi.getConfig().gantt,
clear, clear,
setDateFormat, setDateFormat,

View File

@ -11,19 +11,11 @@
%x href %x href
%x callbackname %x callbackname
%x callbackargs %x callbackargs
%x open_directive
%x type_directive
%x arg_directive
%x close_directive
%x acc_title %x acc_title
%x acc_descr %x acc_descr
%x acc_descr_multiline %x acc_descr_multiline
%% %%
\%\%\{ { this.begin('open_directive'); return 'open_directive'; } \%\%\{ { this.begin('open_directive'); return 'open_directive'; }
<open_directive>((?:(?!\}\%\%)[^:.])*) { this.begin('type_directive'); return 'type_directive'; }
<type_directive>":" { this.popState(); this.begin('arg_directive'); return ':'; }
<type_directive,arg_directive>\}\%\% { this.popState(); this.popState(); return 'close_directive'; }
<arg_directive>((?:(?!\}\%\%).|\n)*) return 'arg_directive';
accTitle\s*":"\s* { this.begin("acc_title");return 'acc_title'; } accTitle\s*":"\s* { this.begin("acc_title");return 'acc_title'; }
<acc_title>(?!\n|;|#)*[^\n]* { this.popState(); return "acc_title_value"; } <acc_title>(?!\n|;|#)*[^\n]* { this.popState(); return "acc_title_value"; }
@ -112,8 +104,7 @@ weekday\s+sunday return 'weekday_sunday'
%% /* language grammar */ %% /* language grammar */
start start
: directive start : gantt document 'EOF' { return $2; }
| gantt document 'EOF' { return $2; }
; ;
document document
@ -155,13 +146,8 @@ statement
| section { yy.addSection($1.substr(8));$$=$1.substr(8); } | section { yy.addSection($1.substr(8));$$=$1.substr(8); }
| clickStatement | clickStatement
| taskTxt taskData {yy.addTask($1,$2);$$='task';} | taskTxt taskData {yy.addTask($1,$2);$$='task';}
| directive
; ;
directive
: openDirective typeDirective closeDirective 'NL'
| openDirective typeDirective ':' argDirective closeDirective 'NL'
;
/* /*
click allows any combination of href and call. click allows any combination of href and call.
@ -192,20 +178,4 @@ clickStatementDebug
| click href {$$=$1 + ' ' + $2;} | click href {$$=$1 + ' ' + $2;}
; ;
openDirective
: open_directive { yy.parseDirective('%%{', 'open_directive'); }
;
typeDirective
: type_directive { yy.parseDirective($1, 'type_directive'); }
;
argDirective
: arg_directive { $1 = $1.trim().replace(/'/g, '"'); yy.parseDirective($1, 'arg_directive'); }
;
closeDirective
: close_directive { yy.parseDirective('}%%', 'close_directive', 'gantt'); }
;
%% %%

View File

@ -1,6 +1,5 @@
import { log } from '../../logger.js'; import { log } from '../../logger.js';
import { random } from '../../utils.js'; import { random } from '../../utils.js';
import mermaidAPI from '../../mermaidAPI.js';
import * as configApi from '../../config.js'; import * as configApi from '../../config.js';
import { getConfig } from '../../config.js'; import { getConfig } from '../../config.js';
import common from '../common/common.js'; import common from '../common/common.js';
@ -33,10 +32,6 @@ function getId() {
return random({ length: 7 }); return random({ length: 7 });
} }
export const parseDirective = function (statement, context, type) {
mermaidAPI.parseDirective(this, statement, context, type);
};
// /** // /**
// * @param currentCommit // * @param currentCommit
// * @param otherCommit // * @param otherCommit
@ -507,7 +502,6 @@ export const commitType = {
}; };
export default { export default {
parseDirective,
getConfig: () => configApi.getConfig().gitGraph, getConfig: () => configApi.getConfig().gitGraph,
setDirection, setDirection,
setOptions, setOptions,

View File

@ -1,87 +1,72 @@
import gitGraphAst from './gitGraphAst.js';
import { parser } from './parser/gitGraph.jison';
// Todo reintroduce without cryptoRandomString describe('when parsing a gitGraph', function () {
import gitGraphAst from './gitGraphAst'; beforeEach(function () {
import { parser } from './parser/gitGraph';
import randomString from 'crypto-random-string';
import cryptoRandomString from 'crypto-random-string';
jest.mock('crypto-random-string');
describe('when parsing a gitGraph', function() {
let randomNumber;
beforeEach(function() {
parser.yy = gitGraphAst; parser.yy = gitGraphAst;
parser.yy.clear(); parser.yy.clear();
randomNumber = 0;
cryptoRandomString.mockImplementation(() => {
randomNumber = randomNumber + 1;
return String(randomNumber);
});
}); });
afterEach(function() { it('should handle a gitGraph definition', function () {
cryptoRandomString.mockReset();
});
it('should handle a gitGraph definition', function() {
const str = 'gitGraph:\n' + 'commit\n'; const str = 'gitGraph:\n' + 'commit\n';
parser.parse(str); parser.parse(str);
const commits = parser.yy.getCommits(); const commits = parser.yy.getCommits();
expect(Object.keys(commits).length).toBe(1); expect(Object.keys(commits).length).toBe(1);
expect(parser.yy.getCurrentBranch()).toBe('master'); expect(parser.yy.getCurrentBranch()).toBe('main');
expect(parser.yy.getDirection()).toBe('LR'); expect(parser.yy.getDirection()).toBe('LR');
expect(Object.keys(parser.yy.getBranches()).length).toBe(1); expect(Object.keys(parser.yy.getBranches()).length).toBe(1);
}); });
it('should handle a gitGraph definition with empty options', function() { it('should handle a gitGraph definition with empty options', function () {
const str = 'gitGraph:\n' + 'options\n' + 'end\n' + 'commit\n'; const str = 'gitGraph:\n' + 'options\n' + ' end\n' + 'commit\n';
parser.parse(str); parser.parse(str);
const commits = parser.yy.getCommits(); const commits = parser.yy.getCommits();
expect(parser.yy.getOptions()).toEqual({}); expect(parser.yy.getOptions()).toEqual({});
expect(Object.keys(commits).length).toBe(1); expect(Object.keys(commits).length).toBe(1);
expect(parser.yy.getCurrentBranch()).toBe('master'); expect(parser.yy.getCurrentBranch()).toBe('main');
expect(parser.yy.getDirection()).toBe('LR'); expect(parser.yy.getDirection()).toBe('LR');
expect(Object.keys(parser.yy.getBranches()).length).toBe(1); expect(Object.keys(parser.yy.getBranches()).length).toBe(1);
}); });
it('should handle a gitGraph definition with valid options', function() { it('should handle a gitGraph definition with valid options', function () {
const str = 'gitGraph:\n' + 'options\n' + '{"key": "value"}\n' + 'end\n' + 'commit\n'; const str = 'gitGraph:\n' + 'options\n' + '{"key": "value"}\n' + 'end\n' + 'commit\n';
parser.parse(str); parser.parse(str);
const commits = parser.yy.getCommits(); const commits = parser.yy.getCommits();
expect(parser.yy.getOptions()['key']).toBe('value'); expect(parser.yy.getOptions()['key']).toBe('value');
expect(Object.keys(commits).length).toBe(1); expect(Object.keys(commits).length).toBe(1);
expect(parser.yy.getCurrentBranch()).toBe('master'); expect(parser.yy.getCurrentBranch()).toBe('main');
expect(parser.yy.getDirection()).toBe('LR'); expect(parser.yy.getDirection()).toBe('LR');
expect(Object.keys(parser.yy.getBranches()).length).toBe(1); expect(Object.keys(parser.yy.getBranches()).length).toBe(1);
}); });
it('should not fail on a gitGraph with malformed json', function() { it('should not fail on a gitGraph with malformed json', function () {
const str = 'gitGraph:\n' + 'options\n' + '{"key": "value"\n' + 'end\n' + 'commit\n'; const str = 'gitGraph:\n' + 'options\n' + '{"key": "value"\n' + 'end\n' + 'commit\n';
parser.parse(str); parser.parse(str);
const commits = parser.yy.getCommits(); const commits = parser.yy.getCommits();
expect(Object.keys(commits).length).toBe(1); expect(Object.keys(commits).length).toBe(1);
expect(parser.yy.getCurrentBranch()).toBe('master'); expect(parser.yy.getCurrentBranch()).toBe('main');
expect(parser.yy.getDirection()).toBe('LR'); expect(parser.yy.getDirection()).toBe('LR');
expect(Object.keys(parser.yy.getBranches()).length).toBe(1); expect(Object.keys(parser.yy.getBranches()).length).toBe(1);
}); });
it('should handle set direction', function() { it('should handle set direction', function () {
const str = 'gitGraph BT:\n' + 'commit\n'; const str = 'gitGraph TB:\n' + 'commit\n';
parser.parse(str); parser.parse(str);
const commits = parser.yy.getCommits(); const commits = parser.yy.getCommits();
expect(Object.keys(commits).length).toBe(1); expect(Object.keys(commits).length).toBe(1);
expect(parser.yy.getCurrentBranch()).toBe('master'); expect(parser.yy.getCurrentBranch()).toBe('main');
expect(parser.yy.getDirection()).toBe('BT'); expect(parser.yy.getDirection()).toBe('TB');
expect(Object.keys(parser.yy.getBranches()).length).toBe(1); expect(Object.keys(parser.yy.getBranches()).length).toBe(1);
}); });
it('should checkout a branch', function() { it('should checkout a branch', function () {
const str = 'gitGraph:\n' + 'branch new\n' + 'checkout new\n'; const str = 'gitGraph:\n' + 'branch new\n' + 'checkout new\n';
parser.parse(str); parser.parse(str);
@ -91,7 +76,7 @@ describe('when parsing a gitGraph', function() {
expect(parser.yy.getCurrentBranch()).toBe('new'); expect(parser.yy.getCurrentBranch()).toBe('new');
}); });
it('should add commits to checked out branch', function() { it('should add commits to checked out branch', function () {
const str = 'gitGraph:\n' + 'branch new\n' + 'checkout new\n' + 'commit\n' + 'commit\n'; const str = 'gitGraph:\n' + 'branch new\n' + 'checkout new\n' + 'commit\n' + 'commit\n';
parser.parse(str); parser.parse(str);
@ -103,7 +88,7 @@ describe('when parsing a gitGraph', function() {
expect(branchCommit).not.toBeNull(); expect(branchCommit).not.toBeNull();
expect(commits[branchCommit].parent).not.toBeNull(); expect(commits[branchCommit].parent).not.toBeNull();
}); });
it('should handle commit with args', function() { it('should handle commit with args', function () {
const str = 'gitGraph:\n' + 'commit "a commit"\n'; const str = 'gitGraph:\n' + 'commit "a commit"\n';
parser.parse(str); parser.parse(str);
@ -112,10 +97,11 @@ describe('when parsing a gitGraph', function() {
expect(Object.keys(commits).length).toBe(1); expect(Object.keys(commits).length).toBe(1);
const key = Object.keys(commits)[0]; const key = Object.keys(commits)[0];
expect(commits[key].message).toBe('a commit'); expect(commits[key].message).toBe('a commit');
expect(parser.yy.getCurrentBranch()).toBe('master'); expect(parser.yy.getCurrentBranch()).toBe('main');
}); });
it('should reset a branch', function() { // Reset has been commented out in JISON
it.skip('should reset a branch', function () {
const str = const str =
'gitGraph:\n' + 'gitGraph:\n' +
'commit\n' + 'commit\n' +
@ -123,18 +109,18 @@ describe('when parsing a gitGraph', function() {
'branch newbranch\n' + 'branch newbranch\n' +
'checkout newbranch\n' + 'checkout newbranch\n' +
'commit\n' + 'commit\n' +
'reset master\n'; 'reset main\n';
parser.parse(str); parser.parse(str);
const commits = parser.yy.getCommits(); const commits = parser.yy.getCommits();
expect(Object.keys(commits).length).toBe(3); expect(Object.keys(commits).length).toBe(3);
expect(parser.yy.getCurrentBranch()).toBe('newbranch'); expect(parser.yy.getCurrentBranch()).toBe('newbranch');
expect(parser.yy.getBranches()['newbranch']).toEqual(parser.yy.getBranches()['master']); expect(parser.yy.getBranches()['newbranch']).toEqual(parser.yy.getBranches()['main']);
expect(parser.yy.getHead().id).toEqual(parser.yy.getBranches()['newbranch']); expect(parser.yy.getHead().id).toEqual(parser.yy.getBranches()['newbranch']);
}); });
it('reset can take an argument', function() { it.skip('reset can take an argument', function () {
const str = const str =
'gitGraph:\n' + 'gitGraph:\n' +
'commit\n' + 'commit\n' +
@ -142,18 +128,18 @@ describe('when parsing a gitGraph', function() {
'branch newbranch\n' + 'branch newbranch\n' +
'checkout newbranch\n' + 'checkout newbranch\n' +
'commit\n' + 'commit\n' +
'reset master^\n'; 'reset main^\n';
parser.parse(str); parser.parse(str);
const commits = parser.yy.getCommits(); const commits = parser.yy.getCommits();
expect(Object.keys(commits).length).toBe(3); expect(Object.keys(commits).length).toBe(3);
expect(parser.yy.getCurrentBranch()).toBe('newbranch'); expect(parser.yy.getCurrentBranch()).toBe('newbranch');
const master = commits[parser.yy.getBranches()['master']]; const main = commits[parser.yy.getBranches()['main']];
expect(parser.yy.getHead().id).toEqual(master.parent); expect(parser.yy.getHead().id).toEqual(main.parent);
}); });
it('should handle fast forwardable merges', function() { it.skip('should handle fast forwardable merges', function () {
const str = const str =
'gitGraph:\n' + 'gitGraph:\n' +
'commit\n' + 'commit\n' +
@ -161,19 +147,19 @@ describe('when parsing a gitGraph', function() {
'checkout newbranch\n' + 'checkout newbranch\n' +
'commit\n' + 'commit\n' +
'commit\n' + 'commit\n' +
'checkout master\n' + 'checkout main\n' +
'merge newbranch\n'; 'merge newbranch\n';
parser.parse(str); parser.parse(str);
const commits = parser.yy.getCommits(); const commits = parser.yy.getCommits();
expect(Object.keys(commits).length).toBe(3); expect(Object.keys(commits).length).toBe(4);
expect(parser.yy.getCurrentBranch()).toBe('master'); expect(parser.yy.getCurrentBranch()).toBe('main');
expect(parser.yy.getBranches()['newbranch']).toEqual(parser.yy.getBranches()['master']); expect(parser.yy.getBranches()['newbranch']).toEqual(parser.yy.getBranches()['main']);
expect(parser.yy.getHead().id).toEqual(parser.yy.getBranches()['newbranch']); expect(parser.yy.getHead().id).toEqual(parser.yy.getBranches()['newbranch']);
}); });
it('should handle cases when merge is a noop', function() { it('should handle cases when merge is a noop', function () {
const str = const str =
'gitGraph:\n' + 'gitGraph:\n' +
'commit\n' + 'commit\n' +
@ -181,18 +167,18 @@ describe('when parsing a gitGraph', function() {
'checkout newbranch\n' + 'checkout newbranch\n' +
'commit\n' + 'commit\n' +
'commit\n' + 'commit\n' +
'merge master\n'; 'merge main\n';
parser.parse(str); parser.parse(str);
const commits = parser.yy.getCommits(); const commits = parser.yy.getCommits();
expect(Object.keys(commits).length).toBe(3); expect(Object.keys(commits).length).toBe(4);
expect(parser.yy.getCurrentBranch()).toBe('newbranch'); expect(parser.yy.getCurrentBranch()).toBe('newbranch');
expect(parser.yy.getBranches()['newbranch']).not.toEqual(parser.yy.getBranches()['master']); expect(parser.yy.getBranches()['newbranch']).not.toEqual(parser.yy.getBranches()['main']);
expect(parser.yy.getHead().id).toEqual(parser.yy.getBranches()['newbranch']); expect(parser.yy.getHead().id).toEqual(parser.yy.getBranches()['newbranch']);
}); });
it('should handle merge with 2 parents', function() { it('should handle merge with 2 parents', function () {
const str = const str =
'gitGraph:\n' + 'gitGraph:\n' +
'commit\n' + 'commit\n' +
@ -200,7 +186,7 @@ describe('when parsing a gitGraph', function() {
'checkout newbranch\n' + 'checkout newbranch\n' +
'commit\n' + 'commit\n' +
'commit\n' + 'commit\n' +
'checkout master\n' + 'checkout main\n' +
'commit\n' + 'commit\n' +
'merge newbranch\n'; 'merge newbranch\n';
@ -208,12 +194,12 @@ describe('when parsing a gitGraph', function() {
const commits = parser.yy.getCommits(); const commits = parser.yy.getCommits();
expect(Object.keys(commits).length).toBe(5); expect(Object.keys(commits).length).toBe(5);
expect(parser.yy.getCurrentBranch()).toBe('master'); expect(parser.yy.getCurrentBranch()).toBe('main');
expect(parser.yy.getBranches()['newbranch']).not.toEqual(parser.yy.getBranches()['master']); expect(parser.yy.getBranches()['newbranch']).not.toEqual(parser.yy.getBranches()['main']);
expect(parser.yy.getHead().id).toEqual(parser.yy.getBranches()['master']); expect(parser.yy.getHead().id).toEqual(parser.yy.getBranches()['main']);
}); });
it('should handle ff merge when history walk has two parents (merge commit)', function() { it.skip('should handle ff merge when history walk has two parents (merge commit)', function () {
const str = const str =
'gitGraph:\n' + 'gitGraph:\n' +
'commit\n' + 'commit\n' +
@ -221,53 +207,25 @@ describe('when parsing a gitGraph', function() {
'checkout newbranch\n' + 'checkout newbranch\n' +
'commit\n' + 'commit\n' +
'commit\n' + 'commit\n' +
'checkout master\n' + 'checkout main\n' +
'commit\n' + 'commit\n' +
'merge newbranch\n' + 'merge newbranch\n' +
'commit\n' + 'commit\n' +
'checkout newbranch\n' + 'checkout newbranch\n' +
'merge master\n'; 'merge main\n';
parser.parse(str); parser.parse(str);
const commits = parser.yy.getCommits(); const commits = parser.yy.getCommits();
expect(Object.keys(commits).length).toBe(6); expect(Object.keys(commits).length).toBe(7);
expect(parser.yy.getCurrentBranch()).toBe('newbranch'); expect(parser.yy.getCurrentBranch()).toBe('newbranch');
expect(parser.yy.getBranches()['newbranch']).toEqual(parser.yy.getBranches()['master']); expect(parser.yy.getBranches()['newbranch']).toEqual(parser.yy.getBranches()['main']);
expect(parser.yy.getHead().id).toEqual(parser.yy.getBranches()['master']); expect(parser.yy.getHead().id).toEqual(parser.yy.getBranches()['main']);
parser.yy.prettyPrint(); parser.yy.prettyPrint();
}); });
it('should generate a secure random ID for commits', function() { it('should generate an array of known branches', function () {
const str = 'gitGraph:\n' + 'commit\n' + 'commit\n';
const EXPECTED_LENGTH = 7;
const EXPECTED_CHARACTERS = '0123456789abcdef';
let idCount = 0;
randomString.mockImplementation(options => {
if (
options.length === EXPECTED_LENGTH &&
options.characters === EXPECTED_CHARACTERS &&
Object.keys(options).length === 2
) {
const id = `abcdef${idCount}`;
idCount += 1;
return id;
}
return 'unexpected-ID';
});
parser.parse(str);
const commits = parser.yy.getCommits();
expect(Object.keys(commits)).toEqual(['abcdef0', 'abcdef1']);
Object.keys(commits).forEach(key => {
expect(commits[key].id).toEqual(key);
});
});
it('should generate an array of known branches', function() {
const str = const str =
'gitGraph:\n' + 'gitGraph:\n' +
'commit\n' + 'commit\n' +
@ -281,7 +239,7 @@ describe('when parsing a gitGraph', function() {
const branches = gitGraphAst.getBranchesAsObjArray(); const branches = gitGraphAst.getBranchesAsObjArray();
expect(branches).toHaveLength(3); expect(branches).toHaveLength(3);
expect(branches[0]).toHaveProperty('name', 'master'); expect(branches[0]).toHaveProperty('name', 'main');
expect(branches[1]).toHaveProperty('name', 'b1'); expect(branches[1]).toHaveProperty('name', 'b1');
expect(branches[2]).toHaveProperty('name', 'b2'); expect(branches[2]).toHaveProperty('name', 'b2');
}); });

View File

@ -1,22 +1,11 @@
/* eslint-env jasmine */
// Todo reintroduce without cryptoRandomString
import gitGraphAst from './gitGraphAst.js'; import gitGraphAst from './gitGraphAst.js';
import { parser } from './parser/gitGraph.jison'; import { parser } from './parser/gitGraph.jison';
//import randomString from 'crypto-random-string';
//import cryptoRandomString from 'crypto-random-string';
//jest.mock('crypto-random-string');
describe('when parsing a gitGraph', function () { describe('when parsing a gitGraph', function () {
let randomNumber;
beforeEach(function () { beforeEach(function () {
parser.yy = gitGraphAst; parser.yy = gitGraphAst;
parser.yy.clear(); parser.yy.clear();
randomNumber = 0;
}); });
// afterEach(function() {
// cryptoRandomString.mockReset();
// });
it('should handle a gitGraph commit with NO pararms, get auto-genrated reandom ID', function () { it('should handle a gitGraph commit with NO pararms, get auto-genrated reandom ID', function () {
const str = `gitGraph: const str = `gitGraph:
commit commit

View File

@ -9,10 +9,6 @@
%x string %x string
%x options %x options
%x open_directive
%x type_directive
%x arg_directive
%x close_directive
%x acc_title %x acc_title
%x acc_descr %x acc_descr
%x acc_descr_multiline %x acc_descr_multiline
@ -20,11 +16,6 @@
%% %%
\%\%\{ { this.begin('open_directive'); return 'open_directive'; }
<open_directive>((?:(?!\}\%\%)[^:.])*) { this.begin('type_directive'); return 'type_directive'; }
<type_directive>":" { this.popState(); this.begin('arg_directive'); return ':'; }
<type_directive,arg_directive>\}\%\% { this.popState(); this.popState(); return 'close_directive'; }
<arg_directive>((?:(?!\}\%\%).|\n)*) return 'arg_directive';
accTitle\s*":"\s* { this.begin("acc_title");return 'acc_title'; } accTitle\s*":"\s* { this.begin("acc_title");return 'acc_title'; }
<acc_title>(?!\n|;|#)*[^\n]* { this.popState(); return "acc_title_value"; } <acc_title>(?!\n|;|#)*[^\n]* { this.popState(); return "acc_title_value"; }
accDescr\s*":"\s* { this.begin("acc_descr");return 'acc_descr'; } accDescr\s*":"\s* { this.begin("acc_descr");return 'acc_descr'; }
@ -76,7 +67,6 @@ checkout(?=\s|$) return 'CHECKOUT';
start start
: eol start : eol start
| directive start
| GG document EOF{ return $3; } | GG document EOF{ return $3; }
| GG ':' document EOF{ return $3; } | GG ':' document EOF{ return $3; }
| GG DIR ':' document EOF {yy.setDirection($2); return $4;} | GG DIR ':' document EOF {yy.setDirection($2); return $4;}
@ -240,27 +230,6 @@ commitType
| HIGHLIGHT { $$=yy.commitType.HIGHLIGHT;} | HIGHLIGHT { $$=yy.commitType.HIGHLIGHT;}
; ;
directive
: openDirective typeDirective closeDirective
| openDirective typeDirective ':' argDirective closeDirective
;
openDirective
: open_directive { yy.parseDirective('%%{', 'open_directive'); }
;
typeDirective
: type_directive { yy.parseDirective($1, 'type_directive'); }
;
argDirective
: arg_directive { $1 = $1.trim().replace(/'/g, '"'); yy.parseDirective($1, 'arg_directive'); }
;
closeDirective
: close_directive { yy.parseDirective('}%%', 'close_directive', 'gitGraph'); }
;
ref ref
: ID : ID
| STR | STR

View File

@ -8,19 +8,10 @@
%x string %x string
%x title %x title
%x open_directive
%x type_directive
%x arg_directive
%x close_directive
%x acc_title %x acc_title
%x acc_descr %x acc_descr
%x acc_descr_multiline %x acc_descr_multiline
%% %%
\%\%\{ { this.begin('open_directive'); return 'open_directive'; }
<open_directive>((?:(?!\}\%\%)[^:.])*) { this.begin('type_directive'); return 'type_directive'; }
<type_directive>":" { this.popState(); this.begin('arg_directive'); return ':'; }
<type_directive,arg_directive>\}\%\% { this.popState(); this.popState(); return 'close_directive'; }
<arg_directive>((?:(?!\}\%\%).|\n)*) return 'arg_directive';
\%\%(?!\{)[^\n]* /* skip comments */ \%\%(?!\{)[^\n]* /* skip comments */
[^\}]\%\%[^\n]* /* skip comments */{ /*console.log('');*/ } [^\}]\%\%[^\n]* /* skip comments */{ /*console.log('');*/ }
[\n\r]+ return 'NEWLINE'; [\n\r]+ return 'NEWLINE';
@ -52,7 +43,6 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multili
start start
: eol start : eol start
| directive start
| PIE document | PIE document
| PIE showData document {yy.setShowData(true);} | PIE showData document {yy.setShowData(true);}
; ;
@ -73,34 +63,12 @@ statement
| acc_title acc_title_value { $$=$2.trim();yy.setAccTitle($$); } | acc_title acc_title_value { $$=$2.trim();yy.setAccTitle($$); }
| acc_descr acc_descr_value { $$=$2.trim();yy.setAccDescription($$); } | acc_descr acc_descr_value { $$=$2.trim();yy.setAccDescription($$); }
| acc_descr_multiline_value { $$=$1.trim();yy.setAccDescription($$); } | section {yy.addSection($1.substr(8));$$=$1.substr(8);} | acc_descr_multiline_value { $$=$1.trim();yy.setAccDescription($$); } | section {yy.addSection($1.substr(8));$$=$1.substr(8);}
| directive
; ;
directive
: openDirective typeDirective closeDirective
| openDirective typeDirective ':' argDirective closeDirective
;
eol eol
: NEWLINE : NEWLINE
| ';' | ';'
| EOF | EOF
; ;
openDirective
: open_directive { yy.parseDirective('%%{', 'open_directive'); }
;
typeDirective
: type_directive { yy.parseDirective($1, 'type_directive'); }
;
argDirective
: arg_directive { $1 = $1.trim().replace(/'/g, '"'); yy.parseDirective($1, 'arg_directive'); }
;
closeDirective
: close_directive { yy.parseDirective('}%%', 'close_directive', 'pie'); }
;
%% %%

View File

@ -62,17 +62,6 @@ describe('pie', () => {
expect(sections['bat']).toBe(40); expect(sections['bat']).toBe(40);
}); });
it('should handle simple pie with a directive', () => {
parser.parse(`%%{init: {'logLevel':0}}%%
pie
"ash" : 60
"bat" : 40
`);
const sections = db.getSections();
expect(sections['ash']).toBe(60);
expect(sections['bat']).toBe(40);
});
it('should handle simple pie with a title', () => { it('should handle simple pie with a title', () => {
parser.parse(`pie title a 60/40 pie parser.parse(`pie title a 60/40 pie
"ash" : 60 "ash" : 60

View File

@ -1,5 +1,4 @@
import { log } from '../../logger.js'; import { log } from '../../logger.js';
import { parseDirective as _parseDirective } from '../../directiveUtils.js';
import { getConfig as commonGetConfig } from '../../config.js'; import { getConfig as commonGetConfig } from '../../config.js';
import { sanitizeText } from '../common/common.js'; import { sanitizeText } from '../common/common.js';
import { import {
@ -11,7 +10,6 @@ import {
setAccDescription, setAccDescription,
clear as commonClear, clear as commonClear,
} from '../common/commonDb.js'; } from '../common/commonDb.js';
import type { ParseDirectiveDefinition } from '../../diagram-api/types.js';
import type { PieFields, PieDB, Sections } from './pieTypes.js'; import type { PieFields, PieDB, Sections } from './pieTypes.js';
import type { RequiredDeep } from 'type-fest'; import type { RequiredDeep } from 'type-fest';
import type { PieDiagramConfig } from '../../config.type.js'; import type { PieDiagramConfig } from '../../config.type.js';
@ -31,10 +29,6 @@ const config: Required<PieDiagramConfig> = structuredClone(DEFAULT_PIE_CONFIG);
const getConfig = (): Required<PieDiagramConfig> => structuredClone(config); const getConfig = (): Required<PieDiagramConfig> => structuredClone(config);
const parseDirective: ParseDirectiveDefinition = (statement, context, type) => {
_parseDirective(this, statement, context, type);
};
const clear = (): void => { const clear = (): void => {
sections = structuredClone(DEFAULT_PIE_DB.sections); sections = structuredClone(DEFAULT_PIE_DB.sections);
showData = DEFAULT_PIE_DB.showData; showData = DEFAULT_PIE_DB.showData;
@ -67,7 +61,6 @@ const getShowData = (): boolean => showData;
export const db: PieDB = { export const db: PieDB = {
getConfig, getConfig,
parseDirective,
clear, clear,
setDiagramTitle, setDiagramTitle,
getDiagramTitle, getDiagramTitle,

View File

@ -1,5 +1,5 @@
import type { PieDiagramConfig } from '../../config.type.js'; import type { PieDiagramConfig } from '../../config.type.js';
import type { DiagramDB, ParseDirectiveDefinition } from '../../diagram-api/types.js'; import type { DiagramDB } from '../../diagram-api/types.js';
export interface PieFields { export interface PieFields {
sections: Sections; sections: Sections;
@ -46,7 +46,6 @@ export interface PieDB extends DiagramDB {
getConfig: () => Required<PieDiagramConfig>; getConfig: () => Required<PieDiagramConfig>;
// common db // common db
parseDirective: ParseDirectiveDefinition;
clear: () => void; clear: () => void;
setDiagramTitle: (title: string) => void; setDiagramTitle: (title: string) => void;
getDiagramTitle: () => string; getDiagramTitle: () => string;

View File

@ -5,10 +5,6 @@
%x string %x string
%x md_string %x md_string
%x title %x title
%x open_directive
%x type_directive
%x arg_directive
%x close_directive
%x acc_title %x acc_title
%x acc_descr %x acc_descr
%x acc_descr_multiline %x acc_descr_multiline
@ -16,11 +12,6 @@
%x point_x %x point_x
%x point_y %x point_y
%% %%
\%\%\{ { this.begin('open_directive'); return 'open_directive'; }
<open_directive>((?:(?!\}\%\%)[^:.])*) { this.begin('type_directive'); return 'type_directive'; }
<type_directive>":" { this.popState(); this.begin('arg_directive'); return ':'; }
<type_directive,arg_directive>\}\%\% { this.popState(); this.popState(); return 'close_directive'; }
<arg_directive>((?:(?!\}\%\%).|\n)*) return 'arg_directive';
\%\%(?!\{)[^\n]* /* skip comments */ \%\%(?!\{)[^\n]* /* skip comments */
[^\}]\%\%[^\n]* /* skip comments */ [^\}]\%\%[^\n]* /* skip comments */
[\n\r]+ return 'NEWLINE'; [\n\r]+ return 'NEWLINE';
@ -87,7 +78,6 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multiline");}
start start
: eol start : eol start
| SPACE start | SPACE start
| directive start
| QUADRANT document | QUADRANT document
; ;
@ -110,7 +100,6 @@ statement
| acc_title acc_title_value { $$=$2.trim();yy.setAccTitle($$); } | acc_title acc_title_value { $$=$2.trim();yy.setAccTitle($$); }
| acc_descr acc_descr_value { $$=$2.trim();yy.setAccDescription($$); } | acc_descr acc_descr_value { $$=$2.trim();yy.setAccDescription($$); }
| acc_descr_multiline_value { $$=$1.trim();yy.setAccDescription($$); } | section {yy.addSection($1.substr(8));$$=$1.substr(8);} | acc_descr_multiline_value { $$=$1.trim();yy.setAccDescription($$); } | section {yy.addSection($1.substr(8));$$=$1.substr(8);}
| directive
; ;
points points
@ -133,33 +122,12 @@ quadrantDetails
| QUADRANT_4 text {yy.setQuadrant4Text($2)} | QUADRANT_4 text {yy.setQuadrant4Text($2)}
; ;
directive
: openDirective typeDirective closeDirective
| openDirective typeDirective ':' argDirective closeDirective
;
eol eol
: NEWLINE : NEWLINE
| SEMI | SEMI
| EOF | EOF
; ;
openDirective
: open_directive { yy.parseDirective('%%{', 'open_directive'); }
;
typeDirective
: type_directive { yy.parseDirective($1, 'type_directive'); }
;
argDirective
: arg_directive { $1 = $1.trim().replace(/'/g, '"'); yy.parseDirective($1, 'arg_directive'); }
;
closeDirective
: close_directive { yy.parseDirective('}%%', 'close_directive', 'quadrantChart'); }
;
text: alphaNumToken text: alphaNumToken
{ $$={text:$1, type: 'text'};} { $$={text:$1, type: 'text'};}
| text textNoTagsToken | text textNoTagsToken

View File

@ -19,7 +19,6 @@ const mockDB: Record<string, Mock<any, any>> = {
setYAxisTopText: vi.fn(), setYAxisTopText: vi.fn(),
setYAxisBottomText: vi.fn(), setYAxisBottomText: vi.fn(),
setDiagramTitle: vi.fn(), setDiagramTitle: vi.fn(),
parseDirective: vi.fn(),
addPoint: vi.fn(), addPoint: vi.fn(),
}; };
@ -45,23 +44,6 @@ describe('Testing quadrantChart jison file', () => {
expect(parserFnConstructor(str)).not.toThrow(); expect(parserFnConstructor(str)).not.toThrow();
}); });
it('should be able to parse directive', () => {
const str =
'%%{init: {"quadrantChart": {"chartWidth": 600, "chartHeight": 600} } }%% \n quadrantChart';
expect(parserFnConstructor(str)).not.toThrow();
expect(mockDB.parseDirective.mock.calls[0]).toEqual(['%%{', 'open_directive']);
expect(mockDB.parseDirective.mock.calls[1]).toEqual(['init', 'type_directive']);
expect(mockDB.parseDirective.mock.calls[2]).toEqual([
'{"quadrantChart": {"chartWidth": 600, "chartHeight": 600} }',
'arg_directive',
]);
expect(mockDB.parseDirective.mock.calls[3]).toEqual([
'}%%',
'close_directive',
'quadrantChart',
]);
});
it('should be able to parse xAxis text', () => { it('should be able to parse xAxis text', () => {
let str = 'quadrantChart\nx-axis urgent --> not urgent'; let str = 'quadrantChart\nx-axis urgent --> not urgent';
expect(parserFnConstructor(str)).not.toThrow(); expect(parserFnConstructor(str)).not.toThrow();
@ -243,8 +225,7 @@ describe('Testing quadrantChart jison file', () => {
}); });
it('should be able to parse the whole chart', () => { it('should be able to parse the whole chart', () => {
const str = `%%{init: {"quadrantChart": {"chartWidth": 600, "chartHeight": 600} } }%% const str = `quadrantChart
quadrantChart
title Analytics and Business Intelligence Platforms title Analytics and Business Intelligence Platforms
x-axis "Completeness of Vision ❤" --> "x-axis-2" x-axis "Completeness of Vision ❤" --> "x-axis-2"
y-axis Ability to Execute --> "y-axis-2" y-axis Ability to Execute --> "y-axis-2"
@ -258,17 +239,6 @@ describe('Testing quadrantChart jison file', () => {
Incorta: [0.20, 0.30]`; Incorta: [0.20, 0.30]`;
expect(parserFnConstructor(str)).not.toThrow(); expect(parserFnConstructor(str)).not.toThrow();
expect(mockDB.parseDirective.mock.calls[0]).toEqual(['%%{', 'open_directive']);
expect(mockDB.parseDirective.mock.calls[1]).toEqual(['init', 'type_directive']);
expect(mockDB.parseDirective.mock.calls[2]).toEqual([
'{"quadrantChart": {"chartWidth": 600, "chartHeight": 600} }',
'arg_directive',
]);
expect(mockDB.parseDirective.mock.calls[3]).toEqual([
'}%%',
'close_directive',
'quadrantChart',
]);
expect(mockDB.setXAxisLeftText).toHaveBeenCalledWith({ expect(mockDB.setXAxisLeftText).toHaveBeenCalledWith({
text: 'Completeness of Vision ❤', text: 'Completeness of Vision ❤',
type: 'text', type: 'text',

View File

@ -1,5 +1,3 @@
import { log } from '../../logger.js';
import mermaidAPI from '../../mermaidAPI.js';
import * as configApi from '../../config.js'; import * as configApi from '../../config.js';
import { sanitizeText } from '../common/common.js'; import { sanitizeText } from '../common/common.js';
import { import {
@ -94,11 +92,6 @@ function getQuadrantData() {
return quadrantBuilder.build(); return quadrantBuilder.build();
} }
export const parseDirective = function (statement: string, context: string, type: string) {
// @ts-ignore: TODO Fix ts errors
mermaidAPI.parseDirective(this, statement, context, type);
};
const clear = function () { const clear = function () {
quadrantBuilder.clear(); quadrantBuilder.clear();
commonClear(); commonClear();
@ -117,7 +110,6 @@ export default {
setYAxisBottomText, setYAxisBottomText,
addPoint, addPoint,
getQuadrantData, getQuadrantData,
parseDirective,
clear, clear,
setAccTitle, setAccTitle,
getAccTitle, getAccTitle,

View File

@ -9,19 +9,10 @@
%x string %x string
%x token %x token
%x unqString %x unqString
%x open_directive
%x type_directive
%x arg_directive
%x close_directive
%x acc_title %x acc_title
%x acc_descr %x acc_descr
%x acc_descr_multiline %x acc_descr_multiline
%% %%
\%\%\{ { this.begin('open_directive'); return 'open_directive'; }
<open_directive>((?:(?!\}\%\%)[^:.])*) { this.begin('type_directive'); return 'type_directive'; }
<type_directive>":" { this.popState(); this.begin('arg_directive'); return ':'; }
<type_directive,arg_directive>\}\%\% { this.popState(); this.popState(); return 'close_directive'; }
<arg_directive>((?:(?!\}\%\%).|\n)*) return 'arg_directive';
"title"\s[^#\n;]+ return 'title'; "title"\s[^#\n;]+ return 'title';
accTitle\s*":"\s* { this.begin("acc_title");return 'acc_title'; } accTitle\s*":"\s* { this.begin("acc_title");return 'acc_title'; }
@ -99,23 +90,10 @@ start
| RD NEWLINE diagram EOF; | RD NEWLINE diagram EOF;
directive directive
: openDirective typeDirective closeDirective : acc_title acc_title_value { $$=$2.trim();yy.setAccTitle($$); }
| openDirective typeDirective ':' argDirective closeDirective
| acc_title acc_title_value { $$=$2.trim();yy.setAccTitle($$); }
| acc_descr acc_descr_value { $$=$2.trim();yy.setAccDescription($$); } | acc_descr acc_descr_value { $$=$2.trim();yy.setAccDescription($$); }
| acc_descr_multiline_value { $$=$1.trim();yy.setAccDescription($$); } | acc_descr_multiline_value { $$=$1.trim();yy.setAccDescription($$); }
; ;
openDirective
: open_directive { yy.parseDirective('%%{', 'open_directive'); };
typeDirective
: type_directive { yy.parseDirective($1, 'type_directive'); };
argDirective
: arg_directive { $1 = $1.trim().replace(/'/g, '"'); yy.parseDirective($1, 'arg_directive'); };
closeDirective
: close_directive { yy.parseDirective('}%%', 'close_directive', 'pie'); };
diagram diagram
: /* empty */ { $$ = [] } : /* empty */ { $$ = [] }

View File

@ -1,6 +1,5 @@
import * as configApi from '../../config.js'; import * as configApi from '../../config.js';
import { log } from '../../logger.js'; import { log } from '../../logger.js';
import mermaidAPI from '../../mermaidAPI.js';
import { import {
setAccTitle, setAccTitle,
@ -48,10 +47,6 @@ const Relationships = {
TRACES: 'traces', TRACES: 'traces',
}; };
export const parseDirective = function (statement, context, type) {
mermaidAPI.parseDirective(this, statement, context, type);
};
const addRequirement = (name, type) => { const addRequirement = (name, type) => {
if (requirements[name] === undefined) { if (requirements[name] === undefined) {
requirements[name] = { requirements[name] = {
@ -149,7 +144,6 @@ export default {
VerifyType, VerifyType,
Relationships, Relationships,
parseDirective,
getConfig: () => configApi.getConfig().req, getConfig: () => configApi.getConfig().req,
addRequirement, addRequirement,

View File

@ -16,22 +16,15 @@
// A special state for grabbing text up to the first comment/newline // A special state for grabbing text up to the first comment/newline
%x ID ALIAS LINE %x ID ALIAS LINE
// Directive states
%x open_directive type_directive arg_directive
%x acc_title %x acc_title
%x acc_descr %x acc_descr
%x acc_descr_multiline %x acc_descr_multiline
%% %%
\%\%\{ { this.begin('open_directive'); return 'open_directive'; }
<open_directive>((?:(?!\}\%\%)[^:.])*) { this.begin('type_directive'); return 'type_directive'; }
<type_directive>":" { this.popState(); this.begin('arg_directive'); return ':'; }
<type_directive,arg_directive>\}\%\% { this.popState(); this.popState(); return 'close_directive'; }
<arg_directive>((?:(?!\}\%\%).|\n)*) return 'arg_directive';
[\n]+ return 'NEWLINE'; [\n]+ return 'NEWLINE';
\s+ /* skip all whitespace */ \s+ /* skip all whitespace */
<ID,ALIAS,LINE>((?!\n)\s)+ /* skip same-line whitespace */ <ID,ALIAS,LINE>((?!\n)\s)+ /* skip same-line whitespace */
<INITIAL,ID,ALIAS,LINE,arg_directive,type_directive,open_directive>\#[^\n]* /* skip comments */ <INITIAL,ID,ALIAS,LINE>\#[^\n]* /* skip comments */
\%%(?!\{)[^\n]* /* skip comments */ \%%(?!\{)[^\n]* /* skip comments */
[^\}]\%\%[^\n]* /* skip comments */ [^\}]\%\%[^\n]* /* skip comments */
[0-9]+(?=[ \n]+) return 'NUM'; [0-9]+(?=[ \n]+) return 'NUM';
@ -106,7 +99,6 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multili
start start
: SPACE start : SPACE start
| NEWLINE start | NEWLINE start
| directive start
| SD document { yy.apply($2);return $2; } | SD document { yy.apply($2);return $2; }
; ;
@ -133,11 +125,6 @@ box_line
; ;
directive
: openDirective typeDirective closeDirective 'NEWLINE'
| openDirective typeDirective ':' argDirective closeDirective 'NEWLINE'
;
statement statement
: participant_statement : participant_statement
| 'create' participant_statement {$2.type='createParticipant'; $$=$2;} | 'create' participant_statement {$2.type='createParticipant'; $$=$2;}
@ -215,7 +202,6 @@ statement
$3.unshift({type: 'breakStart', breakText:yy.parseMessage($2), signalType: yy.LINETYPE.BREAK_START}); $3.unshift({type: 'breakStart', breakText:yy.parseMessage($2), signalType: yy.LINETYPE.BREAK_START});
$3.push({type: 'breakEnd', optText:yy.parseMessage($2), signalType: yy.LINETYPE.BREAK_END}); $3.push({type: 'breakEnd', optText:yy.parseMessage($2), signalType: yy.LINETYPE.BREAK_END});
$$=$3;} $$=$3;}
| directive
; ;
option_sections option_sections
@ -335,20 +321,4 @@ text2
: TXT {$$ = yy.parseMessage($1.trim().substring(1)) } : TXT {$$ = yy.parseMessage($1.trim().substring(1)) }
; ;
openDirective
: open_directive { yy.parseDirective('%%{', 'open_directive'); }
;
typeDirective
: type_directive { yy.parseDirective($1, 'type_directive'); }
;
argDirective
: arg_directive { $1 = $1.trim().replace(/'/g, '"'); yy.parseDirective($1, 'arg_directive'); }
;
closeDirective
: close_directive { yy.parseDirective('}%%', 'close_directive', 'sequence'); }
;
%% %%

View File

@ -1,4 +1,3 @@
import mermaidAPI from '../../mermaidAPI.js';
import * as configApi from '../../config.js'; import * as configApi from '../../config.js';
import { log } from '../../logger.js'; import { log } from '../../logger.js';
import { sanitizeText } from '../common/common.js'; import { sanitizeText } from '../common/common.js';
@ -25,10 +24,6 @@ let currentBox = undefined;
let lastCreated = undefined; let lastCreated = undefined;
let lastDestroyed = undefined; let lastDestroyed = undefined;
export const parseDirective = function (statement, context, type) {
mermaidAPI.parseDirective(this, statement, context, type);
};
export const addBox = function (data) { export const addBox = function (data) {
boxes.push({ boxes.push({
name: data.text, name: data.text,
@ -634,7 +629,6 @@ export default {
getBoxes, getBoxes,
getDiagramTitle, getDiagramTitle,
setDiagramTitle, setDiagramTitle,
parseDirective,
getConfig: () => configApi.getConfig().sequence, getConfig: () => configApi.getConfig().sequence,
clear, clear,
parseMessage, parseMessage,

View File

@ -1,5 +1,4 @@
import { vi } from 'vitest'; import { vi } from 'vitest';
import * as configApi from '../../config.js'; import * as configApi from '../../config.js';
import mermaidAPI from '../../mermaidAPI.js'; import mermaidAPI from '../../mermaidAPI.js';
import { Diagram, getDiagramFromText } from '../../Diagram.js'; import { Diagram, getDiagramFromText } from '../../Diagram.js';
@ -225,6 +224,7 @@ Bob-->Alice: I am good thanks!`;
diagram.renderer.draw(str, 'tst', '1.2.3', diagram); // needs to be rendered for the correct value of visibility auto numbers diagram.renderer.draw(str, 'tst', '1.2.3', diagram); // needs to be rendered for the correct value of visibility auto numbers
expect(diagram.db.showSequenceNumbers()).toBe(true); expect(diagram.db.showSequenceNumbers()).toBe(true);
}); });
it('should handle a sequenceDiagram definition with a title:', async () => { it('should handle a sequenceDiagram definition with a title:', async () => {
const str = ` const str = `
sequenceDiagram sequenceDiagram
@ -2034,90 +2034,3 @@ participant Alice`;
}); });
}); });
}); });
describe('when rendering a sequenceDiagram with directives', () => {
beforeAll(function () {
let conf = {
diagramMarginX: 50,
diagramMarginY: 10,
actorMargin: 50,
width: 150,
height: 65,
boxMargin: 10,
messageMargin: 40,
boxTextMargin: 15,
noteMargin: 25,
};
mermaidAPI.initialize({ sequence: conf });
});
beforeEach(function () {
mermaidAPI.reset();
diagram.renderer.bounds.init();
});
it('should handle one actor, when theme is dark and logLevel is 1 DX1 (dfg1)', async () => {
const str = `
%%{init: { "theme": "dark", "logLevel": 1 } }%%
sequenceDiagram
%%{wrap}%%
participant Alice
`;
diagram = new Diagram(str);
diagram.renderer.bounds.init();
diagram.renderer.draw(str, 'tst', '1.2.3', diagram);
const { bounds, models } = diagram.renderer.bounds.getBounds();
const mermaid = mermaidAPI.getConfig();
expect(mermaid.theme).toBe('dark');
expect(mermaid.logLevel).toBe(1);
expect(bounds.startx).toBe(0);
expect(bounds.startx).toBe(0);
expect(bounds.starty).toBe(0);
expect(bounds.stopy).toBe(
models.lastActor().stopy + models.lastActor().height + mermaid.sequence.boxMargin
);
});
it('should handle one actor, when logLevel is 3 (dfg0)', async () => {
const str = `
%%{initialize: { "logLevel": 3 }}%%
sequenceDiagram
participant Alice
`;
diagram = new Diagram(str);
diagram.renderer.draw(str, 'tst', '1.2.3', diagram);
const { bounds, models } = diagram.renderer.bounds.getBounds();
const mermaid = mermaidAPI.getConfig();
expect(mermaid.logLevel).toBe(3);
expect(bounds.startx).toBe(0);
expect(bounds.startx).toBe(0);
expect(bounds.starty).toBe(0);
expect(bounds.stopy).toBe(
models.lastActor().stopy + models.lastActor().height + mermaid.sequence.boxMargin
);
});
it('should hide sequence numbers when autonumber is removed when autonumber is enabled', async () => {
const str1 = `
sequenceDiagram
autonumber
Alice->Bob:Hello Bob, how are you?
Note right of Bob: Bob thinks
Bob-->Alice: I am good thanks!`;
diagram = new Diagram(str1);
diagram.renderer.draw(str1, 'tst', '1.2.3', diagram); // needs to be rendered for the correct value of visibility auto numbers
expect(diagram.db.showSequenceNumbers()).toBe(true);
const str2 = `
sequenceDiagram
Alice->Bob:Hello Bob, how are you?
Note right of Bob: Bob thinks
Bob-->Alice: I am good thanks!`;
diagram = new Diagram(str2);
diagram.renderer.draw(str2, 'tst', '1.2.3', diagram);
expect(diagram.db.showSequenceNumbers()).toBe(false);
});
});

View File

@ -10,4 +10,7 @@ export const diagram: DiagramDefinition = {
db, db,
renderer, renderer,
styles, styles,
init: ({ wrap }) => {
db.setWrap(wrap);
},
}; };

View File

@ -33,10 +33,6 @@
%x FLOATING_NOTE %x FLOATING_NOTE
%x FLOATING_NOTE_ID %x FLOATING_NOTE_ID
%x struct %x struct
%x open_directive
%x type_directive
%x arg_directive
%x close_directive
// A special state for grabbing text up to the first comment/newline // A special state for grabbing text up to the first comment/newline
%x LINE %x LINE
@ -50,18 +46,13 @@
.*direction\s+RL[^\n]* return 'direction_rl'; .*direction\s+RL[^\n]* return 'direction_rl';
.*direction\s+LR[^\n]* return 'direction_lr'; .*direction\s+LR[^\n]* return 'direction_lr';
\%\%\{ { this.begin('open_directive'); return 'open_directive'; }
<open_directive>((?:(?!\}\%\%)[^:.])*) { this.begin('type_directive'); return 'type_directive'; }
<type_directive>":" { this.popState(); this.begin('arg_directive'); return ':'; }
<type_directive,arg_directive>\}\%\% { this.popState(); this.popState(); return 'close_directive'; }
<arg_directive>((?:(?!\}\%\%).|\n)*) return 'arg_directive';
\%\%(?!\{)[^\n]* /* skip comments */ \%\%(?!\{)[^\n]* /* skip comments */
[^\}]\%\%[^\n]* /* skip comments */{ /*console.log('Crap after close');*/ } [^\}]\%\%[^\n]* /* skip comments */{ /*console.log('Crap after close');*/ }
[\n]+ return 'NL'; [\n]+ return 'NL';
[\s]+ /* skip all whitespace */ [\s]+ /* skip all whitespace */
<ID,STATE,struct,LINE,open_directive,type_directive,arg_directive,close_directive>((?!\n)\s)+ /* skip same-line whitespace */ <ID,STATE,struct,LINE>((?!\n)\s)+ /* skip same-line whitespace */
<INITIAL,ID,STATE,struct,LINE,open_directive,type_directive,arg_directive,close_directive>\#[^\n]* /* skip comments */ <INITIAL,ID,STATE,struct,LINE>\#[^\n]* /* skip comments */
\%%[^\n]* /* skip comments */ \%%[^\n]* /* skip comments */
"scale"\s+ { this.pushState('SCALE'); /* console.log('Got scale', yytext);*/ return 'scale'; } "scale"\s+ { this.pushState('SCALE'); /* console.log('Got scale', yytext);*/ return 'scale'; }
<SCALE>\d+ return 'WIDTH'; <SCALE>\d+ return 'WIDTH';
@ -155,7 +146,6 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multili
start start
: SPACE start : SPACE start
| NL start | NL start
| directive start
| SD document { /* console.log('--> Root document', $2); */ yy.setRootDoc($2); return $2; } | SD document { /* console.log('--> Root document', $2); */ yy.setRootDoc($2); return $2; }
; ;
@ -241,7 +231,6 @@ statement
$$={ stmt: 'state', id: $3.trim(), note:{position: $2.trim(), text: $4.trim()}}; $$={ stmt: 'state', id: $3.trim(), note:{position: $2.trim(), text: $4.trim()}};
} }
| note NOTE_TEXT AS ID | note NOTE_TEXT AS ID
| directive
| direction | direction
| acc_title acc_title_value { $$=$2.trim();yy.setAccTitle($$); } | acc_title acc_title_value { $$=$2.trim();yy.setAccTitle($$); }
| acc_descr acc_descr_value { $$=$2.trim();yy.setAccDescription($$); } | acc_descr acc_descr_value { $$=$2.trim();yy.setAccDescription($$); }
@ -264,10 +253,6 @@ cssClassStatement
} }
; ;
directive
: openDirective typeDirective closeDirective
| openDirective typeDirective ':' argDirective closeDirective
;
direction direction
: direction_tb : direction_tb
{ yy.setDirection('TB');$$={stmt:'dir', value:'TB'};} { yy.setDirection('TB');$$={stmt:'dir', value:'TB'};}
@ -308,20 +293,4 @@ notePosition
| right_of | right_of
; ;
openDirective
: open_directive { yy.parseDirective('%%{', 'open_directive'); }
;
typeDirective
: type_directive { yy.parseDirective($1, 'type_directive'); }
;
argDirective
: arg_directive { $1 = $1.trim().replace(/'/g, '"'); yy.parseDirective($1, 'arg_directive'); }
;
closeDirective
: close_directive { yy.parseDirective('}%%', 'close_directive', 'state'); }
;
%% %%

View File

@ -1,6 +1,5 @@
import { log } from '../../logger.js'; import { log } from '../../logger.js';
import { generateId } from '../../utils.js'; import { generateId } from '../../utils.js';
import mermaidAPI from '../../mermaidAPI.js';
import common from '../common/common.js'; import common from '../common/common.js';
import * as configApi from '../../config.js'; import * as configApi from '../../config.js';
import { import {
@ -77,10 +76,6 @@ export const relationType = {
const clone = (o) => JSON.parse(JSON.stringify(o)); const clone = (o) => JSON.parse(JSON.stringify(o));
export const parseDirective = function (statement, context, type) {
mermaidAPI.parseDirective(this, statement, context, type);
};
const setRootDoc = (o) => { const setRootDoc = (o) => {
log.info('Setting root doc', o); log.info('Setting root doc', o);
// rootDoc = { id: 'root', doc: o }; // rootDoc = { id: 'root', doc: o };
@ -547,7 +542,6 @@ const setDirection = (dir) => {
const trimColon = (str) => (str && str[0] === ':' ? str.substr(1).trim() : str.trim()); const trimColon = (str) => (str && str[0] === ':' ? str.substr(1).trim() : str.trim());
export default { export default {
parseDirective,
getConfig: () => configApi.getConfig().state, getConfig: () => configApi.getConfig().state,
addState, addState,
clear, clear,

View File

@ -55,16 +55,6 @@ describe('state diagram V2, ', function () {
const title = stateDb.getAccTitle(); const title = stateDb.getAccTitle();
expect(title).toBe('a simple title of the diagram'); expect(title).toBe('a simple title of the diagram');
}); });
it('simple with directive', function () {
const str = `%%{init: {'logLevel': 0 }}%%
stateDiagram-v2\n
State1 : this is another string
[*] --> State1
State1 --> [*]
`;
parser.parse(str);
});
it('should handle relation definitions', function () { it('should handle relation definitions', function () {
const str = `stateDiagram-v2\n const str = `stateDiagram-v2\n
[*] --> State1 [*] --> State1

View File

@ -66,16 +66,6 @@ describe('state diagram, ', function () {
const title = stateDb.getAccTitle(); const title = stateDb.getAccTitle();
expect(title).toBe('a simple title of the diagram'); expect(title).toBe('a simple title of the diagram');
}); });
it('simple with directive', function () {
const str = `%%{init: {'logLevel': 0 }}%%
stateDiagram\n
State1 : this is another string
[*] --> State1
State1 --> [*]
`;
parser.parse(str);
});
it('should handle relation definitions', function () { it('should handle relation definitions', function () {
const str = `stateDiagram\n const str = `stateDiagram\n
[*] --> State1 [*] --> State1

View File

@ -9,17 +9,8 @@
%x acc_descr %x acc_descr
%x acc_descr_multiline %x acc_descr_multiline
// Directive states
%x open_directive type_directive arg_directive
%% %%
\%\%\{ { this.begin('open_directive'); return 'open_directive'; }
<open_directive>((?:(?!\}\%\%)[^:.])*) { this.begin('type_directive'); return 'type_directive'; }
<type_directive>":" { this.popState(); this.begin('arg_directive'); return ':'; }
<type_directive,arg_directive>\}\%\% { this.popState(); this.popState(); return 'close_directive'; }
<arg_directive>((?:(?!\}\%\%).|\n)*) return 'arg_directive';
\%%(?!\{)[^\n]* /* skip comments */ \%%(?!\{)[^\n]* /* skip comments */
[^\}]\%\%[^\n]* /* skip comments */ [^\}]\%\%[^\n]* /* skip comments */
[\n]+ return 'NEWLINE'; [\n]+ return 'NEWLINE';
@ -55,7 +46,6 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multili
start start
: timeline document 'EOF' { return $2; } : timeline document 'EOF' { return $2; }
| directive start
; ;
document document
@ -70,11 +60,6 @@ line
| EOF { $$=[];} | EOF { $$=[];}
; ;
directive
: openDirective typeDirective closeDirective 'NEWLINE'
| openDirective typeDirective ':' argDirective closeDirective 'NEWLINE'
;
statement statement
: title {yy.getCommonDb().setDiagramTitle($1.substr(6));$$=$1.substr(6);} : title {yy.getCommonDb().setDiagramTitle($1.substr(6));$$=$1.substr(6);}
| acc_title acc_title_value { $$=$2.trim();yy.getCommonDb().setAccTitle($$); } | acc_title acc_title_value { $$=$2.trim();yy.getCommonDb().setAccTitle($$); }
@ -83,7 +68,6 @@ statement
| section {yy.addSection($1.substr(8));$$=$1.substr(8);} | section {yy.addSection($1.substr(8));$$=$1.substr(8);}
| period_statement | period_statement
| event_statement | event_statement
| directive
; ;
period_statement period_statement
: period {yy.addTask($1,0,'');$$=$1;} : period {yy.addTask($1,0,'');$$=$1;}
@ -92,21 +76,4 @@ event_statement
: event {yy.addEvent($1.substr(2));$$=$1;} : event {yy.addEvent($1.substr(2));$$=$1;}
; ;
openDirective
: open_directive { yy.parseDirective('%%{', 'open_directive'); }
;
typeDirective
: type_directive { yy.parseDirective($1, 'type_directive'); }
;
argDirective
: arg_directive { $1 = $1.trim().replace(/'/g, '"'); yy.parseDirective($1, 'arg_directive'); }
;
closeDirective
: close_directive { yy.parseDirective('}%%', 'close_directive', 'timeline'); }
;
%% %%

View File

@ -1,24 +1,6 @@
import { parser as timeline } from './parser/timeline.jison'; import { parser as timeline } from './parser/timeline.jison';
import * as timelineDB from './timelineDb.js'; import * as timelineDB from './timelineDb.js';
// import { injectUtils } from './mermaidUtils.js'; import { setLogLevel } from '../../diagram-api/diagramAPI.js';
import { parseDirective as _parseDirective } from '../../directiveUtils.js';
import {
log,
setLogLevel,
getConfig,
sanitizeText,
setupGraphViewBox,
} from '../../diagram-api/diagramAPI.js';
// injectUtils(
// log,
// setLogLevel,
// getConfig,
// sanitizeText,
// setupGraphViewBox,
// _parseDirective
// );
describe('when parsing a timeline ', function () { describe('when parsing a timeline ', function () {
beforeEach(function () { beforeEach(function () {

View File

@ -1,4 +1,3 @@
import { parseDirective as _parseDirective } from '../../directiveUtils.js';
import * as commonDb from '../common/commonDb.js'; import * as commonDb from '../common/commonDb.js';
let currentSection = ''; let currentSection = '';
let currentTaskId = 0; let currentTaskId = 0;
@ -9,10 +8,6 @@ const rawTasks = [];
export const getCommonDb = () => commonDb; export const getCommonDb = () => commonDb;
export const parseDirective = (statement, context, type) => {
_parseDirective(this, statement, context, type);
};
export const clear = function () { export const clear = function () {
sections.length = 0; sections.length = 0;
tasks.length = 0; tasks.length = 0;
@ -104,5 +99,4 @@ export default {
addTask, addTask,
addTaskOrg, addTaskOrg,
addEvent, addEvent,
parseDirective,
}; };

View File

@ -1,4 +1,3 @@
import mermaidAPI from '../../mermaidAPI.js';
import * as configApi from '../../config.js'; import * as configApi from '../../config.js';
import { import {
setAccTitle, setAccTitle,
@ -16,10 +15,6 @@ const sections = [];
const tasks = []; const tasks = [];
const rawTasks = []; const rawTasks = [];
export const parseDirective = function (statement, context, type) {
mermaidAPI.parseDirective(this, statement, context, type);
};
export const clear = function () { export const clear = function () {
sections.length = 0; sections.length = 0;
tasks.length = 0; tasks.length = 0;
@ -118,7 +113,6 @@ const getActors = function () {
}; };
export default { export default {
parseDirective,
getConfig: () => configApi.getConfig().journey, getConfig: () => configApi.getConfig().journey,
clear, clear,
setDiagramTitle, setDiagramTitle,

View File

@ -9,17 +9,8 @@
%x acc_descr %x acc_descr
%x acc_descr_multiline %x acc_descr_multiline
// Directive states
%x open_directive type_directive arg_directive
%% %%
\%\%\{ { this.begin('open_directive'); return 'open_directive'; }
<open_directive>((?:(?!\}\%\%)[^:.])*) { this.begin('type_directive'); return 'type_directive'; }
<type_directive>":" { this.popState(); this.begin('arg_directive'); return ':'; }
<type_directive,arg_directive>\}\%\% { this.popState(); this.popState(); return 'close_directive'; }
<arg_directive>((?:(?!\}\%\%).|\n)*) return 'arg_directive';
\%%(?!\{)[^\n]* /* skip comments */ \%%(?!\{)[^\n]* /* skip comments */
[^\}]\%\%[^\n]* /* skip comments */ [^\}]\%\%[^\n]* /* skip comments */
[\n]+ return 'NEWLINE'; [\n]+ return 'NEWLINE';
@ -52,7 +43,6 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multili
start start
: journey document 'EOF' { return $2; } : journey document 'EOF' { return $2; }
| directive start
; ;
document document
@ -67,11 +57,6 @@ line
| EOF { $$=[];} | EOF { $$=[];}
; ;
directive
: openDirective typeDirective closeDirective 'NEWLINE'
| openDirective typeDirective ':' argDirective closeDirective 'NEWLINE'
;
statement statement
: title {yy.setDiagramTitle($1.substr(6));$$=$1.substr(6);} : title {yy.setDiagramTitle($1.substr(6));$$=$1.substr(6);}
| acc_title acc_title_value { $$=$2.trim();yy.setAccTitle($$); } | acc_title acc_title_value { $$=$2.trim();yy.setAccTitle($$); }
@ -79,23 +64,6 @@ statement
| acc_descr_multiline_value { $$=$1.trim();yy.setAccDescription($$); } | acc_descr_multiline_value { $$=$1.trim();yy.setAccDescription($$); }
| section {yy.addSection($1.substr(8));$$=$1.substr(8);} | section {yy.addSection($1.substr(8));$$=$1.substr(8);}
| taskName taskData {yy.addTask($1, $2);$$='task';} | taskName taskData {yy.addTask($1, $2);$$='task';}
| directive
;
openDirective
: open_directive { yy.parseDirective('%%{', 'open_directive'); }
;
typeDirective
: type_directive { yy.parseDirective($1, 'type_directive'); }
;
argDirective
: arg_directive { $1 = $1.trim().replace(/'/g, '"'); yy.parseDirective($1, 'arg_directive'); }
;
closeDirective
: close_directive { yy.parseDirective('}%%', 'close_directive', 'journey'); }
; ;
%% %%

View File

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

View File

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

View File

@ -0,0 +1,65 @@
import { cleanupComments } from './diagram-api/comments.js';
import { extractFrontMatter } from './diagram-api/frontmatter.js';
import type { DiagramMetadata } from './diagram-api/types.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,
};
};
/**
* Preprocess the given code by cleaning it up, extracting front matter and directives,
* cleaning and merging configuration, and removing comments.
* @param code - The code to preprocess.
* @returns The object containing the preprocessed code, title, and configuration.
*/
export function preprocessDiagram(code: string): DiagramMetadata & { 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,
};
}

View File

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

View File

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

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