mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-09-11 19:39:43 +02:00
feat: Add support for config in frontmatter
This commit is contained in:
@@ -14,7 +14,6 @@ describe('Configuration and directives - nodes should be light blue', () => {
|
|||||||
`,
|
`,
|
||||||
{}
|
{}
|
||||||
);
|
);
|
||||||
cy.get('svg');
|
|
||||||
});
|
});
|
||||||
it('Settings from initialize - nodes should be green', () => {
|
it('Settings from initialize - nodes should be green', () => {
|
||||||
imgSnapshotTest(
|
imgSnapshotTest(
|
||||||
@@ -28,7 +27,6 @@ graph TD
|
|||||||
end `,
|
end `,
|
||||||
{ theme: 'forest' }
|
{ theme: 'forest' }
|
||||||
);
|
);
|
||||||
cy.get('svg');
|
|
||||||
});
|
});
|
||||||
it('Settings from initialize overriding themeVariable - nodes should be red', () => {
|
it('Settings from initialize overriding themeVariable - nodes should be red', () => {
|
||||||
imgSnapshotTest(
|
imgSnapshotTest(
|
||||||
@@ -46,7 +44,6 @@ graph TD
|
|||||||
`,
|
`,
|
||||||
{ theme: 'base', themeVariables: { primaryColor: '#ff0000' }, logLevel: 0 }
|
{ theme: 'base', themeVariables: { primaryColor: '#ff0000' }, logLevel: 0 }
|
||||||
);
|
);
|
||||||
cy.get('svg');
|
|
||||||
});
|
});
|
||||||
it('Settings from directive - nodes should be grey', () => {
|
it('Settings from directive - nodes should be grey', () => {
|
||||||
imgSnapshotTest(
|
imgSnapshotTest(
|
||||||
@@ -62,7 +59,24 @@ graph TD
|
|||||||
`,
|
`,
|
||||||
{}
|
{}
|
||||||
);
|
);
|
||||||
cy.get('svg');
|
});
|
||||||
|
it('Settings from frontmatter - nodes should be grey', () => {
|
||||||
|
imgSnapshotTest(
|
||||||
|
`
|
||||||
|
---
|
||||||
|
config:
|
||||||
|
theme: neutral
|
||||||
|
---
|
||||||
|
graph TD
|
||||||
|
A(Start) --> B[/Another/]
|
||||||
|
A[/Another/] --> C[End]
|
||||||
|
subgraph section
|
||||||
|
B
|
||||||
|
C
|
||||||
|
end
|
||||||
|
`,
|
||||||
|
{}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Settings from directive overriding theme variable - nodes should be red', () => {
|
it('Settings from directive overriding theme variable - nodes should be red', () => {
|
||||||
@@ -79,7 +93,6 @@ graph TD
|
|||||||
`,
|
`,
|
||||||
{}
|
{}
|
||||||
);
|
);
|
||||||
cy.get('svg');
|
|
||||||
});
|
});
|
||||||
it('Settings from initialize and directive - nodes should be grey', () => {
|
it('Settings from initialize and directive - nodes should be grey', () => {
|
||||||
imgSnapshotTest(
|
imgSnapshotTest(
|
||||||
@@ -95,7 +108,6 @@ graph TD
|
|||||||
`,
|
`,
|
||||||
{ theme: 'forest' }
|
{ theme: 'forest' }
|
||||||
);
|
);
|
||||||
cy.get('svg');
|
|
||||||
});
|
});
|
||||||
it('Theme from initialize, directive overriding theme variable - nodes should be red', () => {
|
it('Theme from initialize, directive overriding theme variable - nodes should be red', () => {
|
||||||
imgSnapshotTest(
|
imgSnapshotTest(
|
||||||
@@ -111,8 +123,50 @@ graph TD
|
|||||||
`,
|
`,
|
||||||
{ theme: 'base' }
|
{ theme: 'base' }
|
||||||
);
|
);
|
||||||
cy.get('svg');
|
|
||||||
});
|
});
|
||||||
|
it('Theme from initialize, frontmatter overriding theme variable - nodes should be red', () => {
|
||||||
|
imgSnapshotTest(
|
||||||
|
`
|
||||||
|
---
|
||||||
|
config:
|
||||||
|
theme: base
|
||||||
|
themeVariables:
|
||||||
|
primaryColor: '#ff0000'
|
||||||
|
---
|
||||||
|
graph TD
|
||||||
|
A(Start) --> B[/Another/]
|
||||||
|
A[/Another/] --> C[End]
|
||||||
|
subgraph section
|
||||||
|
B
|
||||||
|
C
|
||||||
|
end
|
||||||
|
`,
|
||||||
|
{ theme: 'forest' }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render if values are not quoted properly', () => {
|
||||||
|
// #ff0000 is not quoted properly, and will evaluate to null.
|
||||||
|
// This test ensures that the rendering still works.
|
||||||
|
imgSnapshotTest(
|
||||||
|
`---
|
||||||
|
config:
|
||||||
|
theme: base
|
||||||
|
themeVariables:
|
||||||
|
primaryColor: #ff0000
|
||||||
|
---
|
||||||
|
graph TD
|
||||||
|
A(Start) --> B[/Another/]
|
||||||
|
A[/Another/] --> C[End]
|
||||||
|
subgraph section
|
||||||
|
B
|
||||||
|
C
|
||||||
|
end
|
||||||
|
`,
|
||||||
|
{ theme: 'forest' }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('Theme variable from initialize, theme from directive - nodes should be red', () => {
|
it('Theme variable from initialize, theme from directive - nodes should be red', () => {
|
||||||
imgSnapshotTest(
|
imgSnapshotTest(
|
||||||
`
|
`
|
||||||
@@ -127,13 +181,11 @@ graph TD
|
|||||||
`,
|
`,
|
||||||
{ themeVariables: { primaryColor: '#ff0000' } }
|
{ themeVariables: { primaryColor: '#ff0000' } }
|
||||||
);
|
);
|
||||||
cy.get('svg');
|
|
||||||
});
|
});
|
||||||
describe('when rendering several diagrams', () => {
|
describe('when rendering several diagrams', () => {
|
||||||
it('diagrams should not taint later diagrams', () => {
|
it('diagrams should not taint later diagrams', () => {
|
||||||
const url = 'http://localhost:9000/theme-directives.html';
|
const url = 'http://localhost:9000/theme-directives.html';
|
||||||
cy.visit(url);
|
cy.visit(url);
|
||||||
cy.get('svg');
|
|
||||||
cy.matchImageSnapshot('conf-and-directives.spec-when-rendering-several-diagrams-diagram-1');
|
cy.matchImageSnapshot('conf-and-directives.spec-when-rendering-several-diagrams-diagram-1');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -48,7 +48,7 @@ export class Diagram {
|
|||||||
// extractFrontMatter().
|
// extractFrontMatter().
|
||||||
|
|
||||||
this.parser.parse = (text: string) =>
|
this.parser.parse = (text: string) =>
|
||||||
originalParse(cleanupComments(extractFrontMatter(text, this.db)));
|
originalParse(cleanupComments(extractFrontMatter(text, this.db, configApi.addDirective)));
|
||||||
|
|
||||||
this.parser.parser.yy = this.db;
|
this.parser.parser.yy = this.db;
|
||||||
this.init = diagram.init;
|
this.init = diagram.init;
|
||||||
|
@@ -144,9 +144,12 @@ export const getConfig = (): MermaidConfig => {
|
|||||||
* @param options - The potential setConfig parameter
|
* @param options - The potential setConfig parameter
|
||||||
*/
|
*/
|
||||||
export const sanitize = (options: any) => {
|
export const sanitize = (options: any) => {
|
||||||
|
if (!options) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
// Checking that options are not in the list of excluded options
|
// Checking that options are not in the list of excluded options
|
||||||
['secure', ...(siteConfig.secure ?? [])].forEach((key) => {
|
['secure', ...(siteConfig.secure ?? [])].forEach((key) => {
|
||||||
if (options[key] !== undefined) {
|
if (Object.hasOwn(options, key)) {
|
||||||
// DO NOT attempt to print options[key] within `${}` as a malicious script
|
// DO NOT attempt to print options[key] within `${}` as a malicious script
|
||||||
// can exploit the logger's attempt to stringify the value and execute arbitrary code
|
// can exploit the logger's attempt to stringify the value and execute arbitrary code
|
||||||
log.debug(`Denied attempt to modify a secure key ${key}`, options[key]);
|
log.debug(`Denied attempt to modify a secure key ${key}`, options[key]);
|
||||||
@@ -156,7 +159,7 @@ export const sanitize = (options: any) => {
|
|||||||
|
|
||||||
// Check that there no attempts of prototype pollution
|
// Check that there no attempts of prototype pollution
|
||||||
Object.keys(options).forEach((key) => {
|
Object.keys(options).forEach((key) => {
|
||||||
if (key.indexOf('__') === 0) {
|
if (key.startsWith('__')) {
|
||||||
delete options[key];
|
delete options[key];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@@ -2,8 +2,13 @@ import { vi } from 'vitest';
|
|||||||
import { extractFrontMatter } from './frontmatter.js';
|
import { extractFrontMatter } from './frontmatter.js';
|
||||||
|
|
||||||
const dbMock = () => ({ setDiagramTitle: vi.fn() });
|
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', dbMock())).toEqual('diagram');
|
||||||
});
|
});
|
||||||
@@ -75,4 +80,21 @@ describe('extractFrontmatter', () => {
|
|||||||
'tag suffix cannot contain exclamation marks'
|
'tag suffix cannot contain exclamation marks'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('handles frontmatter with config', () => {
|
||||||
|
const text = `---
|
||||||
|
title: hello
|
||||||
|
config:
|
||||||
|
graph:
|
||||||
|
string: hello
|
||||||
|
number: 14
|
||||||
|
boolean: false
|
||||||
|
array: [1, 2, 3]
|
||||||
|
---
|
||||||
|
diagram`;
|
||||||
|
expect(extractFrontMatter(text, {}, setConfigMock)).toEqual('diagram');
|
||||||
|
expect(setConfigMock).toHaveBeenCalledWith({
|
||||||
|
graph: { string: 'hello', number: 14, boolean: false, array: [1, 2, 3] },
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
|
import { MermaidConfig } from '../config.type.js';
|
||||||
import { DiagramDB } from './types.js';
|
import { 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';
|
||||||
@@ -9,38 +10,50 @@ import * as yaml from 'js-yaml';
|
|||||||
// Relevant YAML spec: https://yaml.org/spec/1.2.2/#914-explicit-documents
|
// Relevant YAML spec: https://yaml.org/spec/1.2.2/#914-explicit-documents
|
||||||
export const frontMatterRegex = /^-{3}\s*[\n\r](.*?)[\n\r]-{3}\s*[\n\r]+/s;
|
export const frontMatterRegex = /^-{3}\s*[\n\r](.*?)[\n\r]-{3}\s*[\n\r]+/s;
|
||||||
|
|
||||||
type FrontMatterMetadata = {
|
interface FrontMatterMetadata {
|
||||||
title?: string;
|
title?: string;
|
||||||
// Allows custom display modes. Currently used for compact mode in gantt charts.
|
// Allows custom display modes. Currently used for compact mode in gantt charts.
|
||||||
displayMode?: string;
|
displayMode?: string;
|
||||||
};
|
config?: MermaidConfig;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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 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(text: string, db: DiagramDB): string {
|
export function extractFrontMatter(
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
const parsed: FrontMatterMetadata = yaml.load(matches[1], {
|
const parsed: FrontMatterMetadata = yaml.load(matches[1], {
|
||||||
// To keep things simple, only allow strings, arrays, and plain objects.
|
// To support config, we need JSON schema.
|
||||||
// https://www.yaml.org/spec/1.2/spec.html#id2802346
|
// https://www.yaml.org/spec/1.2/spec.html#id2803231
|
||||||
schema: yaml.FAILSAFE_SCHEMA,
|
schema: yaml.JSON_SCHEMA,
|
||||||
}) as FrontMatterMetadata;
|
}) as FrontMatterMetadata;
|
||||||
|
|
||||||
if (parsed?.title) {
|
if (parsed?.title) {
|
||||||
db.setDiagramTitle?.(parsed.title);
|
// toString() is necessary because YAML could parse the title as a number/boolean
|
||||||
|
db.setDiagramTitle?.(parsed.title.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parsed?.displayMode) {
|
if (parsed?.displayMode) {
|
||||||
db.setDisplayMode?.(parsed.displayMode);
|
// toString() is necessary because YAML could parse the title as a number/boolean
|
||||||
|
db.setDisplayMode?.(parsed.displayMode.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsed?.config) {
|
||||||
|
setDiagramConfig?.(parsed.config);
|
||||||
}
|
}
|
||||||
|
|
||||||
return text.slice(matches[0].length);
|
return text.slice(matches[0].length);
|
||||||
} else {
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -30,6 +30,7 @@ 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 { parseDirective } from './directiveUtils.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 = [
|
||||||
@@ -385,7 +386,12 @@ const render = async function (
|
|||||||
|
|
||||||
configApi.reset();
|
configApi.reset();
|
||||||
|
|
||||||
// Add Directives. Must do this before getting the config and before creating the diagram.
|
// 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);
|
const graphInit = utils.detectInit(text);
|
||||||
if (graphInit) {
|
if (graphInit) {
|
||||||
configApi.addDirective(graphInit);
|
configApi.addDirective(graphInit);
|
||||||
|
@@ -96,7 +96,10 @@ const directiveWithoutOpen =
|
|||||||
* @param config - Optional mermaid configuration object.
|
* @param config - Optional mermaid configuration object.
|
||||||
* @returns The json object representing the init passed to mermaid.initialize()
|
* @returns The json object representing the init passed to mermaid.initialize()
|
||||||
*/
|
*/
|
||||||
export const detectInit = function (text: string, config?: MermaidConfig): MermaidConfig {
|
export const detectInit = function (
|
||||||
|
text: string,
|
||||||
|
config?: MermaidConfig
|
||||||
|
): MermaidConfig | undefined {
|
||||||
const inits = detectDirective(text, /(?:init\b)|(?:initialize\b)/);
|
const inits = detectDirective(text, /(?:init\b)|(?:initialize\b)/);
|
||||||
let results = {};
|
let results = {};
|
||||||
|
|
||||||
@@ -106,7 +109,11 @@ export const detectInit = function (text: string, config?: MermaidConfig): Merma
|
|||||||
} else {
|
} else {
|
||||||
results = inits.args;
|
results = inits.args;
|
||||||
}
|
}
|
||||||
if (results) {
|
|
||||||
|
if (!results) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let type = detectType(text, config);
|
let type = detectType(text, config);
|
||||||
['config'].forEach((prop) => {
|
['config'].forEach((prop) => {
|
||||||
if (results[prop] !== undefined) {
|
if (results[prop] !== undefined) {
|
||||||
@@ -117,9 +124,7 @@ export const detectInit = function (text: string, config?: MermaidConfig): Merma
|
|||||||
delete results[prop];
|
delete results[prop];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
// Todo: refactor this, these results are never used
|
|
||||||
return results;
|
return results;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -844,7 +849,7 @@ export const sanitizeDirective = (args: unknown): void => {
|
|||||||
log.debug('sanitizeDirective called with', args);
|
log.debug('sanitizeDirective called with', args);
|
||||||
|
|
||||||
// Return if not an object
|
// Return if not an object
|
||||||
if (typeof args !== 'object') {
|
if (typeof args !== 'object' || args == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -861,7 +866,8 @@ export const sanitizeDirective = (args: unknown): void => {
|
|||||||
key.startsWith('__') ||
|
key.startsWith('__') ||
|
||||||
key.includes('proto') ||
|
key.includes('proto') ||
|
||||||
key.includes('constr') ||
|
key.includes('constr') ||
|
||||||
!configKeys.has(key)
|
!configKeys.has(key) ||
|
||||||
|
args[key] == null
|
||||||
) {
|
) {
|
||||||
log.debug('sanitize deleting key: ', key);
|
log.debug('sanitize deleting key: ', key);
|
||||||
delete args[key];
|
delete args[key];
|
||||||
|
Reference in New Issue
Block a user