Merge branch 'develop' into sidv/tinyMermaid

* develop: (189 commits)
  Remove unnecessary tests
  Remove optional chaining
  chore: Update docs
  refactor: Use `||` instead of `??`
  Update flowchart.md (#4798)
  Update flowchart.md (#4792)
  core: Adapt changes from 3f7bafb2d7
  Update cypress/helpers/util.js
  chore: Update docs
  chore: Add deprecation notices, improve types
  chore: Cleanup gitGraph tests
  chore: Fix unit tests
  chore(deps): update all patch dependencies
  chore: Update docs
  Update docs
  New Mermaid Live Editor for Confluence Cloud (#4814)
  Update link to Discourse theme component (#4811)
  Update flowchart.md (#4810)
  chore: remove unneeded `CommomDB`
  chore: Update docs
  ...
This commit is contained in:
Sidharth Vinod
2023-09-08 16:32:53 +05:30
202 changed files with 5091 additions and 4098 deletions

View File

@@ -1 +1 @@
../mermaid/src/docs/syntax/zenuml.md
../mermaid/src/docs/syntax/zenuml.md

22
packages/mermaid/.madgerc Normal file
View File

@@ -0,0 +1,22 @@
{
"detectiveOptions": {
"ts": {
"skipTypeImports": true
},
"es6": {
"skipTypeImports": true
}
},
"fileExtensions": [
"js",
"ts"
],
"excludeRegExp": [
"node_modules",
"docs",
"vitepress",
"detector",
"Detector"
],
"tsConfig": "./tsconfig.json"
}

View File

@@ -38,6 +38,7 @@
"docs:verify-version": "ts-node-esm scripts/update-release-version.mts --verify",
"types:build-config": "ts-node-esm --transpileOnly scripts/create-types-from-json-schema.mts",
"types:verify-config": "ts-node-esm scripts/create-types-from-json-schema.mts --verify",
"checkCircle": "npx madge --circular ./src",
"release": "pnpm build",
"prepublishOnly": "cpy '../../README.*' ./ --cwd=. && pnpm -w run build"
},
@@ -82,7 +83,9 @@
"@types/cytoscape": "^3.19.9",
"@types/d3": "^7.4.0",
"@types/d3-sankey": "^0.12.1",
"@types/d3-scale": "^4.0.3",
"@types/d3-selection": "^3.0.5",
"@types/d3-shape": "^3.1.1",
"@types/dompurify": "^3.0.2",
"@types/jsdom": "^21.1.1",
"@types/lodash-es": "^4.17.7",
@@ -111,7 +114,8 @@
"remark-gfm": "^3.0.1",
"rimraf": "^5.0.0",
"start-server-and-test": "^2.0.0",
"typedoc": "^0.24.5",
"type-fest": "^4.1.0",
"typedoc": "^0.25.0",
"typedoc-plugin-markdown": "^3.15.2",
"typescript": "^5.0.4",
"unist-util-flatmap": "^1.0.0",

View File

@@ -2,11 +2,9 @@ import * as configApi from './config.js';
import { log } from './logger.js';
import { getDiagram, registerDiagram } from './diagram-api/diagramAPI.js';
import { detectType, getDiagramLoader } from './diagram-api/detectType.js';
import { extractFrontMatter } from './diagram-api/frontmatter.js';
import { UnknownDiagramError } from './errors.js';
import { cleanupComments } from './diagram-api/comments.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;
@@ -22,7 +20,7 @@ export class Diagram {
private init?: DiagramDefinition['init'];
private detectError?: UnknownDiagramError;
constructor(public text: string) {
constructor(public text: string, public metadata: Pick<DiagramMetadata, 'title'> = {}) {
this.text += '\n';
const cnf = configApi.getConfig();
try {
@@ -37,19 +35,6 @@ export class Diagram {
this.db = diagram.db;
this.renderer = diagram.renderer;
this.parser = diagram.parser;
const originalParse = this.parser.parse.bind(this.parser);
// Wrap the jison parse() method to handle extracting frontmatter.
//
// This can't be done in this.parse() because some code
// directly calls diagram.parser.parse(), bypassing this.parse().
//
// Similarly, we can't do this in getDiagramFromText() because some code
// calls diagram.db.clear(), which would reset anything set by
// extractFrontMatter().
this.parser.parse = (text: string) =>
originalParse(cleanupComments(extractFrontMatter(text, this.db)));
this.parser.parser.yy = this.db;
this.init = diagram.init;
this.parse();
@@ -60,7 +45,12 @@ export class Diagram {
throw this.detectError;
}
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);
}
@@ -82,11 +72,15 @@ export class Diagram {
* **Warning:** This function may be changed in the future.
* @alpha
* @param text - The mermaid diagram definition.
* @param metadata - Diagram metadata, defined in YAML.
* @returns A the Promise of a Diagram object.
* @throws {@link UnknownDiagramError} if the diagram type can not be found.
* @privateRemarks This is exported as part of the public mermaidAPI.
*/
export const getDiagramFromText = async (text: string): Promise<Diagram> => {
export const getDiagramFromText = async (
text: string,
metadata: Pick<DiagramMetadata, 'title'> = {}
): Promise<Diagram> => {
const type = detectType(text, configApi.getConfig());
try {
// Trying to find the diagram
@@ -101,5 +95,5 @@ export const getDiagramFromText = async (text: string): Promise<Diagram> => {
const { id, diagram } = await loader();
registerDiagram(id, diagram);
}
return new Diagram(text);
return new Diagram(text, metadata);
};

View File

@@ -13,7 +13,6 @@ export const mermaidAPI = {
svg: '<svg></svg>',
}),
parse: mAPI.parse,
parseDirective: vi.fn(),
initialize: vi.fn(),
getConfig: configApi.getConfig,
setConfig: configApi.setConfig,

View File

@@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/**
* assignWithDepth Extends the functionality of {@link ObjectConstructor.assign} with the
* assignWithDepth Extends the functionality of {@link Object.assign} with the
* ability to merge arbitrary-depth objects For each key in src with path `k` (recursively)
* performs an Object.assign(dst[`k`], src[`k`]) with a slight change from the typical handling of
* undefined for dst[`k`]: instead of raising an error, dst[`k`] is auto-initialized to `{}` and

View File

@@ -1,47 +0,0 @@
import { sanitizeText as _sanitizeText } from './diagrams/common/common.js';
import { getConfig } from './config.js';
let title = '';
let diagramTitle = '';
let description = '';
const sanitizeText = (txt: string): string => _sanitizeText(txt, getConfig());
export const clear = function (): void {
title = '';
description = '';
diagramTitle = '';
};
export const setAccTitle = function (txt: string): void {
title = sanitizeText(txt).replace(/^\s+/g, '');
};
export const getAccTitle = function (): string {
return title || diagramTitle;
};
export const setAccDescription = function (txt: string): void {
description = sanitizeText(txt).replace(/\n\s+/g, '\n');
};
export const getAccDescription = function (): string {
return description;
};
export const setDiagramTitle = function (txt: string): void {
diagramTitle = sanitizeText(txt);
};
export const getDiagramTitle = function (): string {
return diagramTitle;
};
export default {
getAccTitle,
setAccTitle,
getDiagramTitle,
setDiagramTitle,
getAccDescription,
setAccDescription,
clear,
};

View File

@@ -1,11 +1,13 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import * as configApi from './config.js';
import type { MermaidConfig } from './config.type.js';
describe('when working with site config', function () {
describe('when working with site config', () => {
beforeEach(() => {
// Resets the site config to default config
configApi.setSiteConfig({});
});
it('should set site config and config properly', function () {
it('should set site config and config properly', () => {
const config_0 = { fontFamily: 'foo-font', fontSize: 150 };
configApi.setSiteConfig(config_0);
const config_1 = configApi.getSiteConfig();
@@ -14,19 +16,26 @@ describe('when working with site config', function () {
expect(config_1.fontSize).toEqual(config_0.fontSize);
expect(config_1).toEqual(config_2);
});
it('should respect secure keys when applying directives', function () {
const config_0 = {
it('should respect secure keys when applying directives', () => {
const config_0: MermaidConfig = {
fontFamily: 'foo-font',
securityLevel: 'strict', // can't be changed
fontSize: 12345, // can't be changed
secure: [...configApi.defaultConfig.secure!, 'fontSize'],
};
configApi.setSiteConfig(config_0);
const directive = { fontFamily: 'baf', fontSize: 54321 /* fontSize shouldn't be changed */ };
const cfg = configApi.updateCurrentConfig(config_0, [directive]);
const directive: MermaidConfig = {
fontFamily: 'baf',
// fontSize and securityLevel shouldn't be changed
fontSize: 54321,
securityLevel: 'loose',
};
const cfg: MermaidConfig = configApi.updateCurrentConfig(config_0, [directive]);
expect(cfg.fontFamily).toEqual(directive.fontFamily);
expect(cfg.fontSize).toBe(config_0.fontSize);
expect(cfg.securityLevel).toBe(config_0.securityLevel);
});
it('should allow setting partial options', function () {
it('should allow setting partial options', () => {
const defaultConfig = configApi.getConfig();
configApi.setConfig({
@@ -42,7 +51,7 @@ describe('when working with site config', function () {
updatedConfig.quadrantChart!.chartWidth
);
});
it('should set reset config properly', function () {
it('should set reset config properly', () => {
const config_0 = { fontFamily: 'foo-font', fontSize: 150 };
configApi.setSiteConfig(config_0);
const config_1 = { fontFamily: 'baf' };
@@ -55,7 +64,7 @@ describe('when working with site config', function () {
const config_4 = configApi.getSiteConfig();
expect(config_4.fontFamily).toEqual(config_0.fontFamily);
});
it('should set global reset config properly', function () {
it('should set global reset config properly', () => {
const config_0 = { fontFamily: 'foo-font', fontSize: 150 };
configApi.setSiteConfig(config_0);
const config_1 = configApi.getSiteConfig();

View File

@@ -3,15 +3,16 @@ import { log } from './logger.js';
import theme from './themes/index.js';
import config from './defaultConfig.js';
import type { MermaidConfig } from './config.type.js';
import { sanitizeDirective } from './utils/sanitizeDirective.js';
export const defaultConfig: MermaidConfig = Object.freeze(config);
let siteConfig: MermaidConfig = assignWithDepth({}, defaultConfig);
let configFromInitialize: MermaidConfig;
let directives: any[] = [];
let directives: MermaidConfig[] = [];
let currentConfig: MermaidConfig = assignWithDepth({}, defaultConfig);
export const updateCurrentConfig = (siteCfg: MermaidConfig, _directives: any[]) => {
export const updateCurrentConfig = (siteCfg: MermaidConfig, _directives: MermaidConfig[]) => {
// start with config being the siteConfig
let cfg: MermaidConfig = assignWithDepth({}, siteCfg);
// let sCfg = assignWithDepth(defaultConfig, siteConfigDelta);
@@ -20,7 +21,6 @@ export const updateCurrentConfig = (siteCfg: MermaidConfig, _directives: any[])
let sumOfDirectives: MermaidConfig = {};
for (const d of _directives) {
sanitize(d);
// Apply the data from the directive where the the overrides the themeVariables
sumOfDirectives = assignWithDepth(sumOfDirectives, d);
}
@@ -111,12 +111,6 @@ export const getSiteConfig = (): MermaidConfig => {
* @returns The currentConfig merged with the sanitized conf
*/
export const setConfig = (conf: MermaidConfig): MermaidConfig => {
// sanitize(conf);
// Object.keys(conf).forEach(key => {
// const manipulator = manipulators[key];
// conf[key] = manipulator ? manipulator(conf[key]) : conf[key];
// });
checkConfig(conf);
assignWithDepth(currentConfig, conf);
@@ -150,9 +144,12 @@ export const getConfig = (): MermaidConfig => {
* @param options - The potential setConfig parameter
*/
export const sanitize = (options: any) => {
if (!options) {
return;
}
// Checking that options are not in the list of excluded options
['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
// 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]);
@@ -162,7 +159,7 @@ export const sanitize = (options: any) => {
// Check that there no attempts of prototype pollution
Object.keys(options).forEach((key) => {
if (key.indexOf('__') === 0) {
if (key.startsWith('__')) {
delete options[key];
}
});
@@ -188,16 +185,14 @@ export const sanitize = (options: any) => {
*
* @param directive - The directive to push in
*/
export const addDirective = (directive: any) => {
if (directive.fontFamily) {
if (!directive.themeVariables) {
directive.themeVariables = { fontFamily: directive.fontFamily };
} else {
if (!directive.themeVariables.fontFamily) {
directive.themeVariables = { fontFamily: directive.fontFamily };
}
}
export const addDirective = (directive: MermaidConfig) => {
sanitizeDirective(directive);
// If the directive has a fontFamily, but no themeVariables, add the fontFamily to the themeVariables
if (directive.fontFamily && (!directive.themeVariables || !directive.themeVariables.fontFamily)) {
directive.themeVariables = { fontFamily: directive.fontFamily };
}
directives.push(directive);
updateCurrentConfig(siteConfig, directives);
};

View File

@@ -1048,7 +1048,7 @@ export interface GanttDiagramConfig extends BaseDiagramConfig {
* Pattern is:
*
* ```javascript
* /^([1-9][0-9]*)(minute|hour|day|week|month)$/
* /^([1-9][0-9]*)(millisecond|second|minute|hour|day|week|month)$/
* ```
*
*/
@@ -1298,6 +1298,21 @@ export interface SankeyDiagramConfig extends BaseDiagramConfig {
*/
nodeAlignment?: 'left' | 'right' | 'center' | 'justify';
useMaxWidth?: boolean;
/**
* Toggle to display or hide values along with title.
*
*/
showValues?: boolean;
/**
* The prefix to use for values
*
*/
prefix?: string;
/**
* The suffix to use for values
*
*/
suffix?: string;
}
/**
* This interface was referenced by `MermaidConfig`'s JSON-Schema

View File

@@ -368,7 +368,20 @@ const cutPathAtIntersect = (_points, boundryNode) => {
return points;
};
//(edgePaths, e, edge, clusterDb, diagramtype, graph)
/**
* Calculate the deltas and angle between two points
* @param {{x: number, y:number}} point1
* @param {{x: number, y:number}} point2
* @returns {{angle: number, deltaX: number, deltaY: number}}
*/
function calculateDeltaAndAngle(point1, point2) {
const [x1, y1] = [point1.x, point1.y];
const [x2, y2] = [point2.x, point2.y];
const deltaX = x2 - x1;
const deltaY = y2 - y1;
return { angle: Math.atan(deltaY / deltaX), deltaX, deltaY };
}
export const insertEdge = function (elem, e, edge, clusterDb, diagramType, graph) {
let points = edge.points;
let pointsHasChanged = false;
@@ -435,22 +448,62 @@ export const insertEdge = function (elem, e, edge, clusterDb, diagramType, graph
const lineData = points.filter((p) => !Number.isNaN(p.y));
// This is the accessor function we talked about above
let curve;
let curve = curveBasis;
// Currently only flowcharts get the curve from the settings, perhaps this should
// be expanded to a common setting? Restricting it for now in order not to cause side-effects that
// have not been thought through
if (diagramType === 'graph' || diagramType === 'flowchart') {
curve = edge.curve || curveBasis;
} else {
curve = curveBasis;
if (edge.curve && (diagramType === 'graph' || diagramType === 'flowchart')) {
curve = edge.curve;
}
// curve = curveLinear;
// We need to draw the lines a bit shorter to avoid drawing
// under any transparent markers.
// The offsets are calculated from the markers' dimensions.
const markerOffsets = {
aggregation: 18,
extension: 18,
composition: 18,
dependency: 6,
lollipop: 13.5,
arrow_point: 5.3,
};
const lineFunction = line()
.x(function (d) {
return d.x;
.x(function (d, i, data) {
let offset = 0;
if (i === 0 && Object.hasOwn(markerOffsets, edge.arrowTypeStart)) {
// Handle first point
// Calculate the angle and delta between the first two points
const { angle, deltaX } = calculateDeltaAndAngle(data[0], data[1]);
// Calculate the offset based on the angle and the marker's dimensions
offset = markerOffsets[edge.arrowTypeStart] * Math.cos(angle) * (deltaX >= 0 ? 1 : -1) || 0;
} else if (i === data.length - 1 && Object.hasOwn(markerOffsets, edge.arrowTypeEnd)) {
// Handle last point
// Calculate the angle and delta between the last two points
const { angle, deltaX } = calculateDeltaAndAngle(
data[data.length - 1],
data[data.length - 2]
);
offset = markerOffsets[edge.arrowTypeEnd] * Math.cos(angle) * (deltaX >= 0 ? 1 : -1) || 0;
}
return d.x + offset;
})
.y(function (d) {
return d.y;
.y(function (d, i, data) {
// Same handling as X above
let offset = 0;
if (i === 0 && Object.hasOwn(markerOffsets, edge.arrowTypeStart)) {
const { angle, deltaY } = calculateDeltaAndAngle(data[0], data[1]);
offset =
markerOffsets[edge.arrowTypeStart] * Math.abs(Math.sin(angle)) * (deltaY >= 0 ? 1 : -1);
} else if (i === data.length - 1 && Object.hasOwn(markerOffsets, edge.arrowTypeEnd)) {
const { angle, deltaY } = calculateDeltaAndAngle(
data[data.length - 1],
data[data.length - 2]
);
offset =
markerOffsets[edge.arrowTypeEnd] * Math.abs(Math.sin(angle)) * (deltaY >= 0 ? 1 : -1);
}
return d.y + offset;
})
.curve(curve);

View File

@@ -155,9 +155,9 @@ export const render = async (elem, graph, markers, diagramtype, id) => {
clearClusters();
clearGraphlib();
log.warn('Graph at first:', graphlibJson.write(graph));
log.warn('Graph at first:', JSON.stringify(graphlibJson.write(graph)));
adjustClustersAndEdges(graph);
log.warn('Graph after:', graphlibJson.write(graph));
log.warn('Graph after:', JSON.stringify(graphlibJson.write(graph)));
// log.warn('Graph ever after:', graphlibJson.write(graph.node('A').graph));
await recursiveRender(elem, graph, diagramtype);
};

View File

@@ -16,7 +16,7 @@ const extension = (elem, type, id) => {
.append('marker')
.attr('id', type + '-extensionStart')
.attr('class', 'marker extension ' + type)
.attr('refX', 0)
.attr('refX', 18)
.attr('refY', 7)
.attr('markerWidth', 190)
.attr('markerHeight', 240)
@@ -29,7 +29,7 @@ const extension = (elem, type, id) => {
.append('marker')
.attr('id', type + '-extensionEnd')
.attr('class', 'marker extension ' + type)
.attr('refX', 19)
.attr('refX', 1)
.attr('refY', 7)
.attr('markerWidth', 20)
.attr('markerHeight', 28)
@@ -44,7 +44,7 @@ const composition = (elem, type) => {
.append('marker')
.attr('id', type + '-compositionStart')
.attr('class', 'marker composition ' + type)
.attr('refX', 0)
.attr('refX', 18)
.attr('refY', 7)
.attr('markerWidth', 190)
.attr('markerHeight', 240)
@@ -57,7 +57,7 @@ const composition = (elem, type) => {
.append('marker')
.attr('id', type + '-compositionEnd')
.attr('class', 'marker composition ' + type)
.attr('refX', 19)
.attr('refX', 1)
.attr('refY', 7)
.attr('markerWidth', 20)
.attr('markerHeight', 28)
@@ -71,7 +71,7 @@ const aggregation = (elem, type) => {
.append('marker')
.attr('id', type + '-aggregationStart')
.attr('class', 'marker aggregation ' + type)
.attr('refX', 0)
.attr('refX', 18)
.attr('refY', 7)
.attr('markerWidth', 190)
.attr('markerHeight', 240)
@@ -84,7 +84,7 @@ const aggregation = (elem, type) => {
.append('marker')
.attr('id', type + '-aggregationEnd')
.attr('class', 'marker aggregation ' + type)
.attr('refX', 19)
.attr('refX', 1)
.attr('refY', 7)
.attr('markerWidth', 20)
.attr('markerHeight', 28)
@@ -98,7 +98,7 @@ const dependency = (elem, type) => {
.append('marker')
.attr('id', type + '-dependencyStart')
.attr('class', 'marker dependency ' + type)
.attr('refX', 0)
.attr('refX', 6)
.attr('refY', 7)
.attr('markerWidth', 190)
.attr('markerHeight', 240)
@@ -111,7 +111,7 @@ const dependency = (elem, type) => {
.append('marker')
.attr('id', type + '-dependencyEnd')
.attr('class', 'marker dependency ' + type)
.attr('refX', 19)
.attr('refX', 13)
.attr('refY', 7)
.attr('markerWidth', 20)
.attr('markerHeight', 28)
@@ -125,15 +125,32 @@ const lollipop = (elem, type) => {
.append('marker')
.attr('id', type + '-lollipopStart')
.attr('class', 'marker lollipop ' + type)
.attr('refX', 0)
.attr('refX', 13)
.attr('refY', 7)
.attr('markerWidth', 190)
.attr('markerHeight', 240)
.attr('orient', 'auto')
.append('circle')
.attr('stroke', 'black')
.attr('fill', 'white')
.attr('cx', 6)
.attr('fill', 'transparent')
.attr('cx', 7)
.attr('cy', 7)
.attr('r', 6);
elem
.append('defs')
.append('marker')
.attr('id', type + '-lollipopEnd')
.attr('class', 'marker lollipop ' + type)
.attr('refX', 1)
.attr('refY', 7)
.attr('markerWidth', 190)
.attr('markerHeight', 240)
.attr('orient', 'auto')
.append('circle')
.attr('stroke', 'black')
.attr('fill', 'transparent')
.attr('cx', 7)
.attr('cy', 7)
.attr('r', 6);
};
@@ -143,7 +160,7 @@ const point = (elem, type) => {
.attr('id', type + '-pointEnd')
.attr('class', 'marker ' + type)
.attr('viewBox', '0 0 10 10')
.attr('refX', 10)
.attr('refX', 6)
.attr('refY', 5)
.attr('markerUnits', 'userSpaceOnUse')
.attr('markerWidth', 12)

View File

@@ -291,8 +291,8 @@ export const adjustClustersAndEdges = (graph, depth) => {
shape: 'labelRect',
style: '',
});
const edge1 = JSON.parse(JSON.stringify(edge));
const edge2 = JSON.parse(JSON.stringify(edge));
const edge1 = structuredClone(edge);
const edge2 = structuredClone(edge);
edge1.label = '';
edge1.arrowTypeEnd = 'none';
edge2.label = '';

View File

@@ -5,7 +5,6 @@ import { getConfig } from '../config.js';
import intersect from './intersect/index.js';
import createLabel from './createLabel.js';
import note from './shapes/note.js';
import { parseMember } from '../diagrams/class/svgDraw.js';
import { evaluate } from '../diagrams/common/common.js';
const formatClass = (str) => {
@@ -880,8 +879,8 @@ const class_box = (parent, node) => {
maxWidth = classTitleBBox.width;
}
const classAttributes = [];
node.classData.members.forEach((str) => {
const parsedInfo = parseMember(str);
node.classData.members.forEach((member) => {
const parsedInfo = member.getDisplayDetails();
let parsedText = parsedInfo.displayText;
if (getConfig().flowchart.htmlLabels) {
parsedText = parsedText.replace(/</g, '&lt;').replace(/>/g, '&gt;');
@@ -914,8 +913,8 @@ const class_box = (parent, node) => {
maxHeight += lineHeight;
const classMethods = [];
node.classData.methods.forEach((str) => {
const parsedInfo = parseMember(str);
node.classData.methods.forEach((member) => {
const parsedInfo = member.getDisplayDetails();
let displayText = parsedInfo.displayText;
if (getConfig().flowchart.htmlLabels) {
displayText = displayText.replace(/</g, '&lt;').replace(/>/g, '&gt;');

View File

@@ -1,5 +1,7 @@
import type { RequiredDeep } from 'type-fest';
import theme from './themes/index.js';
import { type MermaidConfig } from './config.type.js';
import type { MermaidConfig } from './config.type.js';
// Uses our custom Vite jsonSchemaPlugin to load only the default values from
// our JSON Schema
@@ -13,7 +15,7 @@ import defaultConfigJson from './schemas/config.schema.yaml?only-defaults=true';
* Non-JSON JS default values are listed in this file, e.g. functions, or
* `undefined` (explicitly set so that `configKeys` finds them).
*/
const config: Partial<MermaidConfig> = {
const config: RequiredDeep<MermaidConfig> = {
...defaultConfigJson,
// Set, even though they're `undefined` so that `configKeys` finds these keys
// TODO: Should we replace these with `null` so that they can go in the JSON Schema?
@@ -232,7 +234,7 @@ const config: Partial<MermaidConfig> = {
},
pie: {
...defaultConfigJson.pie,
useWidth: undefined,
useWidth: 984,
},
requirement: {
...defaultConfigJson.requirement,
@@ -263,5 +265,5 @@ const keyify = (obj: any, prefix = ''): string[] =>
return [...res, prefix + el];
}, []);
export const configKeys: string[] = keyify(config, '');
export const configKeys: Set<string> = new Set(keyify(config, ''));
export default config;

View File

@@ -4,5 +4,5 @@
* @returns cleaned text
*/
export const cleanupComments = (text: string): string => {
return text.trimStart().replace(/^\s*%%(?!{)[^\n]+\n?/gm, '');
return text.replace(/^\s*%%(?!{)[^\n]+\n?/gm, '').trimStart();
};

View File

@@ -1,4 +1,4 @@
import { MermaidConfig } from '../config.type.js';
import type { MermaidConfig } from '../config.type.js';
import { log } from '../logger.js';
import type {
DetectorRecord,
@@ -6,14 +6,10 @@ import type {
DiagramLoader,
ExternalDiagramDefinition,
} from './types.js';
import { frontMatterRegex } from './frontmatter.js';
import { getDiagram, registerDiagram } from './diagramAPI.js';
import { anyCommentRegex, directiveRegex, frontMatterRegex } from './regexes.js';
import { UnknownDiagramError } from '../errors.js';
const directive = /%{2}{\s*(?:(\w+)\s*:|(\w+))\s*(?:(\w+)|((?:(?!}%{2}).|\r?\n)*))?\s*(?:}%{2})?/gi;
const anyComment = /\s*%%.*\n/gm;
const detectors: Record<string, DetectorRecord> = {};
export const detectors: Record<string, DetectorRecord> = {};
/**
* Detects the type of the graph text.
@@ -38,7 +34,10 @@ const detectors: Record<string, DetectorRecord> = {};
* @returns A graph definition key
*/
export const detectType = function (text: string, config?: MermaidConfig): string {
text = text.replace(frontMatterRegex, '').replace(directive, '').replace(anyComment, '\n');
text = text
.replace(frontMatterRegex, '')
.replace(directiveRegex, '')
.replace(anyCommentRegex, '\n');
for (const [key, { detector }] of Object.entries(detectors)) {
const diagram = detector(text, config);
if (diagram) {
@@ -70,39 +69,6 @@ export const registerLazyLoadedDiagrams = (...diagrams: ExternalDiagramDefinitio
}
};
export const loadRegisteredDiagrams = async () => {
log.debug(`Loading registered diagrams`);
// Load all lazy loaded diagrams in parallel
const results = await Promise.allSettled(
Object.entries(detectors).map(async ([key, { detector, loader }]) => {
if (loader) {
try {
getDiagram(key);
} catch (error) {
try {
// Register diagram if it is not already registered
const { diagram, id } = await loader();
registerDiagram(id, diagram, detector);
} catch (err) {
// Remove failed diagram from detectors
log.error(`Failed to load external diagram with key ${key}. Removing from detectors.`);
delete detectors[key];
throw err;
}
}
}
})
);
const failed = results.filter((result) => result.status === 'rejected');
if (failed.length > 0) {
log.error(`Failed to load ${failed.length} external diagrams`);
for (const res of failed) {
log.error(res);
}
throw new Error(`Failed to load ${failed.length} external diagrams`);
}
};
export const addDetector = (key: string, detector: DiagramDetector, loader?: DiagramLoader) => {
if (detectors[key]) {
log.error(`Detector with key ${key} already exists`);

View File

@@ -5,7 +5,7 @@ import er from '../diagrams/er/erDetector.js';
import git from '../diagrams/git/gitGraphDetector.js';
import gantt from '../diagrams/gantt/ganttDetector.js';
import { info } from '../diagrams/info/infoDetector.js';
import pie from '../diagrams/pie/pieDetector.js';
import { pie } from '../diagrams/pie/pieDetector.js';
import quadrantChart from '../diagrams/quadrant-chart/quadrantDetector.js';
import requirement from '../diagrams/requirement/requirementDetector.js';
import sequence from '../diagrams/sequence/sequenceDetector.js';

View File

@@ -1,7 +1,7 @@
import { detectType } from './detectType.js';
import { getDiagram, registerDiagram } from './diagramAPI.js';
import { addDiagrams } from './diagram-orchestration.js';
import { DiagramDetector } from './types.js';
import type { DiagramDetector } from './types.js';
import { getDiagramFromText } from '../Diagram.js';
import { it, describe, expect, beforeAll } from 'vitest';

View File

@@ -4,9 +4,8 @@ import { getConfig as _getConfig } from '../config.js';
import { sanitizeText as _sanitizeText } from '../diagrams/common/common.js';
import { setupGraphViewbox as _setupGraphViewbox } from '../setupGraphViewbox.js';
import { addStylesForDiagram } from '../styles.js';
import { DiagramDefinition, DiagramDetector } from './types.js';
import * as _commonDb from '../commonDb.js';
import { parseDirective as _parseDirective } from '../directiveUtils.js';
import type { DiagramDefinition, DiagramDetector } from './types.js';
import * as _commonDb from '../diagrams/common/commonDb.js';
/*
Packaging and exposing resources for external diagrams so that they can import
@@ -21,8 +20,6 @@ export const setupGraphViewbox = _setupGraphViewbox;
export const getCommonDb = () => {
return _commonDb;
};
export const parseDirective = (p: any, statement: string, context: string, type: string) =>
_parseDirective(p, statement, context, type);
const diagrams: Record<string, DiagramDefinition> = {};
export interface Detectors {
@@ -52,17 +49,18 @@ export const registerDiagram = (
}
addStylesForDiagram(id, diagram.styles);
if (diagram.injectUtils) {
diagram.injectUtils(
log,
setLogLevel,
getConfig,
sanitizeText,
setupGraphViewbox,
getCommonDb(),
parseDirective
);
}
diagram.injectUtils?.(
log,
setLogLevel,
getConfig,
sanitizeText,
setupGraphViewbox,
getCommonDb(),
() => {
// 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 => {

View File

@@ -1,78 +1,171 @@
import { vi } from 'vitest';
import { extractFrontMatter } from './frontmatter.js';
const dbMock = () => ({ setDiagramTitle: vi.fn() });
describe('extractFrontmatter', () => {
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', () => {
const text = `---\ntitle: foo\ndiagram`;
expect(extractFrontMatter(text, dbMock())).toEqual(text);
expect(extractFrontMatter(text)).toMatchInlineSnapshot(`
{
"metadata": {},
"text": "---
title: foo
diagram",
}
`);
});
it('handles empty frontmatter', () => {
const db = dbMock();
const text = `---\n\n---\ndiagram`;
expect(extractFrontMatter(text, db)).toEqual('diagram');
expect(db.setDiagramTitle).not.toHaveBeenCalled();
expect(extractFrontMatter(text)).toMatchInlineSnapshot(`
{
"metadata": {},
"text": "diagram",
}
`);
});
it('handles frontmatter without mappings', () => {
const db = dbMock();
const text = `---\n1\n---\ndiagram`;
expect(extractFrontMatter(text, db)).toEqual('diagram');
expect(db.setDiagramTitle).not.toHaveBeenCalled();
expect(extractFrontMatter(`---\n1\n---\ndiagram`)).toMatchInlineSnapshot(`
{
"metadata": {},
"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', () => {
const db = dbMock();
const text = `diagram\n---\ntitle: foo\n---\n`;
expect(extractFrontMatter(text, db)).toEqual(text);
expect(db.setDiagramTitle).not.toHaveBeenCalled();
expect(extractFrontMatter(text)).toMatchInlineSnapshot(`
{
"metadata": {},
"text": "diagram
---
title: foo
---
",
}
`);
});
it('handles frontmatter with multiple delimiters', () => {
const db = dbMock();
const text = `---\ntitle: foo---bar\n---\ndiagram\n---\ntest`;
expect(extractFrontMatter(text, db)).toEqual('diagram\n---\ntest');
expect(db.setDiagramTitle).toHaveBeenCalledWith('foo---bar');
expect(extractFrontMatter(text)).toMatchInlineSnapshot(`
{
"metadata": {
"title": "foo---bar",
},
"text": "diagram
---
test",
}
`);
});
it('handles frontmatter with multi-line string and multiple delimiters', () => {
const db = dbMock();
const text = `---\ntitle: |\n multi-line string\n ---\n---\ndiagram`;
expect(extractFrontMatter(text, db)).toEqual('diagram');
expect(db.setDiagramTitle).toHaveBeenCalledWith('multi-line string\n---\n');
expect(extractFrontMatter(text)).toMatchInlineSnapshot(`
{
"metadata": {
"title": "multi-line string
---
",
},
"text": "diagram",
}
`);
});
it('handles frontmatter with title', () => {
const db = dbMock();
const text = `---\ntitle: foo\n---\ndiagram`;
expect(extractFrontMatter(text, db)).toEqual('diagram');
expect(db.setDiagramTitle).toHaveBeenCalledWith('foo');
expect(extractFrontMatter(text)).toMatchInlineSnapshot(`
{
"metadata": {
"title": "foo",
},
"text": "diagram",
}
`);
});
it('handles booleans in frontmatter properly', () => {
const db = dbMock();
const text = `---\ntitle: true\n---\ndiagram`;
expect(extractFrontMatter(text, db)).toEqual('diagram');
expect(db.setDiagramTitle).toHaveBeenCalledWith('true');
expect(extractFrontMatter(text)).toMatchInlineSnapshot(`
{
"metadata": {
"title": "true",
},
"text": "diagram",
}
`);
});
it('ignores unspecified frontmatter keys', () => {
const db = dbMock();
const text = `---\ninvalid: true\ntitle: foo\ntest: bar\n---\ndiagram`;
expect(extractFrontMatter(text, db)).toEqual('diagram');
expect(db.setDiagramTitle).toHaveBeenCalledWith('foo');
expect(extractFrontMatter(text)).toMatchInlineSnapshot(`
{
"metadata": {
"title": "foo",
},
"text": "diagram",
}
`);
});
it('throws exception for invalid YAML syntax', () => {
const text = `---\n!!!\n---\ndiagram`;
expect(() => extractFrontMatter(text, dbMock())).toThrow(
'tag suffix cannot contain exclamation marks'
);
expect(() => extractFrontMatter(text)).toThrow('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)).toMatchInlineSnapshot(`
{
"metadata": {
"config": {
"graph": {
"array": [
1,
2,
3,
],
"boolean": false,
"number": 14,
"string": "hello",
},
},
"title": "hello",
},
"text": "diagram",
}
`);
});
});

View File

@@ -1,46 +1,60 @@
import { DiagramDB } from './types.js';
import type { MermaidConfig } from '../config.type.js';
import { frontMatterRegex } from './regexes.js';
// The "* as yaml" part is necessary for tree-shaking
import * as yaml from 'js-yaml';
// Match Jekyll-style front matter blocks (https://jekyllrb.com/docs/front-matter/).
// Based on regex used by Jekyll: https://github.com/jekyll/jekyll/blob/6dd3cc21c40b98054851846425af06c64f9fb466/lib/jekyll/document.rb#L10
// Note that JS doesn't support the "\A" anchor, which means we can't use
// multiline mode.
// 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;
type FrontMatterMetadata = {
interface FrontMatterMetadata {
title?: string;
// Allows custom display modes. Currently used for compact mode in gantt charts.
displayMode?: string;
};
config?: MermaidConfig;
}
export interface FrontMatterResult {
text: string;
metadata: FrontMatterMetadata;
}
/**
* Extract and parse frontmatter from text, if present, and sets appropriate
* properties in the provided db.
* @param text - The text that may have a YAML frontmatter.
* @param db - Diagram database, could be of any diagram.
* @returns text with frontmatter stripped out
*/
export function extractFrontMatter(text: string, db: DiagramDB): string {
export function extractFrontMatter(text: string): FrontMatterResult {
const matches = text.match(frontMatterRegex);
if (matches) {
const parsed: FrontMatterMetadata = yaml.load(matches[1], {
// To keep things simple, only allow strings, arrays, and plain objects.
// https://www.yaml.org/spec/1.2/spec.html#id2802346
schema: yaml.FAILSAFE_SCHEMA,
}) as FrontMatterMetadata;
if (parsed?.title) {
db.setDiagramTitle?.(parsed.title);
}
if (parsed?.displayMode) {
db.setDisplayMode?.(parsed.displayMode);
}
return text.slice(matches[0].length);
} else {
return text;
if (!matches) {
return {
text,
metadata: {},
};
}
let parsed: FrontMatterMetadata =
yaml.load(matches[1], {
// To support config, we need JSON schema.
// https://www.yaml.org/spec/1.2/spec.html#id2803231
schema: yaml.JSON_SCHEMA,
}) ?? {};
// To handle runtime data type changes
parsed = typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
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;
}
return {
text: text.slice(matches[0].length),
metadata,
};
}

View File

@@ -0,0 +1,36 @@
import { log } from '../logger.js';
import { detectors } from './detectType.js';
import { getDiagram, registerDiagram } from './diagramAPI.js';
export const loadRegisteredDiagrams = async () => {
log.debug(`Loading registered diagrams`);
// Load all lazy loaded diagrams in parallel
const results = await Promise.allSettled(
Object.entries(detectors).map(async ([key, { detector, loader }]) => {
if (loader) {
try {
getDiagram(key);
} catch (error) {
try {
// Register diagram if it is not already registered
const { diagram, id } = await loader();
registerDiagram(id, diagram, detector);
} catch (err) {
// Remove failed diagram from detectors
log.error(`Failed to load external diagram with key ${key}. Removing from detectors.`);
delete detectors[key];
throw err;
}
}
}
})
);
const failed = results.filter((result) => result.status === 'rejected');
if (failed.length > 0) {
log.error(`Failed to load ${failed.length} external diagrams`);
for (const res of failed) {
log.error(res);
}
throw new Error(`Failed to load ${failed.length} external diagrams`);
}
};

View File

@@ -0,0 +1,11 @@
// Match Jekyll-style front matter blocks (https://jekyllrb.com/docs/front-matter/).
// Based on regex used by Jekyll: https://github.com/jekyll/jekyll/blob/6dd3cc21c40b98054851846425af06c64f9fb466/lib/jekyll/document.rb#L10
// Note that JS doesn't support the "\A" anchor, which means we can't use
// multiline mode.
// 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 directiveRegex =
/%{2}{\s*(?:(\w+)\s*:|(\w+))\s*(?:(\w+)|((?:(?!}%{2}).|\r?\n)*))?\s*(?:}%{2})?/gi;
export const anyCommentRegex = /\s*%%.*\n/gm;

View File

@@ -1,7 +1,13 @@
import { Diagram } from '../Diagram.js';
import type { MermaidConfig } from '../config.type.js';
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { Diagram } from '../Diagram.js';
import type { BaseDiagramConfig, MermaidConfig } from '../config.type.js';
import type * as d3 from 'd3';
export interface DiagramMetadata {
title?: string;
config?: MermaidConfig;
}
export interface InjectUtils {
_log: any;
_setLogLevel: any;
@@ -9,6 +15,7 @@ export interface InjectUtils {
_sanitizeText: any;
_setupGraphViewbox: any;
_commonDb: any;
/** @deprecated as directives will be pre-processed since https://github.com/mermaid-js/mermaid/pull/4759 */
_parseDirective: any;
}
@@ -16,11 +23,19 @@ export interface InjectUtils {
* Generic Diagram DB that may apply to any diagram type.
*/
export interface DiagramDB {
// config
getConfig?: () => BaseDiagramConfig | undefined;
// db
clear?: () => void;
setDiagramTitle?: (title: string) => void;
setDisplayMode?: (title: string) => void;
getDiagramTitle?: () => string;
setAccTitle?: (title: string) => void;
getAccTitle?: () => string;
setAccDescription?: (describetion: string) => void;
getAccDescription?: () => string;
setDisplayMode?: (title: string) => void;
bindFunctions?: (element: Element) => void;
}
@@ -37,6 +52,7 @@ export interface DiagramDefinition {
_sanitizeText: InjectUtils['_sanitizeText'],
_setupGraphViewbox: InjectUtils['_setupGraphViewbox'],
_commonDb: InjectUtils['_commonDb'],
/** @deprecated as directives will be pre-processed since https://github.com/mermaid-js/mermaid/pull/4759 */
_parseDirective: InjectUtils['_parseDirective']
) => void;
}
@@ -75,15 +91,6 @@ export interface ParserDefinition {
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 SVG = d3.Selection<SVGSVGElement, unknown, Element | null, unknown>;

View File

@@ -1,7 +1,11 @@
import mermaidAPI from '../../mermaidAPI.js';
import * as configApi from '../../config.js';
import { sanitizeText } from '../common/common.js';
import { setAccTitle, getAccTitle, getAccDescription, setAccDescription } from '../../commonDb.js';
import {
setAccTitle,
getAccTitle,
getAccDescription,
setAccDescription,
} from '../common/commonDb.js';
let c4ShapeArray = [];
let boundaryParseStack = [''];
@@ -33,10 +37,6 @@ export const setC4Type = function (c4TypeParam) {
c4Type = sanitizedText;
};
export const parseDirective = function (statement, context, type) {
mermaidAPI.parseDirective(this, statement, context, type);
};
//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
@@ -816,7 +816,6 @@ export default {
getAccTitle,
getAccDescription,
setAccDescription,
parseDirective,
getConfig: () => configApi.getConfig().c4,
clear,
LINETYPE,

View File

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

View File

@@ -72,25 +72,16 @@
%x string_kv_key
%x string_kv_value
%x open_directive
%x type_directive
%x arg_directive
%x close_directive
%x acc_title
%x acc_descr
%x acc_descr_multiline
%%
\%\%\{ { this.begin('open_directive'); return 'open_directive'; }
.*direction\s+TB[^\n]* return 'direction_tb';
.*direction\s+BT[^\n]* return 'direction_bt';
.*direction\s+RL[^\n]* return 'direction_rl';
.*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';
@@ -207,7 +198,6 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multiline");}
start
: mermaidDoc
| direction
| directive start
;
direction
@@ -225,26 +215,6 @@ mermaidDoc
: 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
: C4_CONTEXT NEWLINE statements EOF {yy.setC4Type($1)}

View File

@@ -1,10 +1,9 @@
// @ts-nocheck - don't check until handle it
import { select, Selection } from 'd3';
import type { Selection } from 'd3';
import { select } from 'd3';
import { log } from '../../logger.js';
import * as configApi from '../../config.js';
import common from '../common/common.js';
import utils from '../../utils.js';
import mermaidAPI from '../../mermaidAPI.js';
import {
setAccTitle,
getAccTitle,
@@ -13,8 +12,9 @@ import {
clear as commonClear,
setDiagramTitle,
getDiagramTitle,
} from '../../commonDb.js';
import {
} from '../common/commonDb.js';
import { ClassMember } from './classTypes.js';
import type {
ClassRelation,
ClassNode,
ClassNote,
@@ -36,11 +36,6 @@ let functions: any[] = [];
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) {
let genericType = '';
let className = id;
@@ -70,21 +65,21 @@ export const setClassLabel = function (id: string, label: string) {
* @public
*/
export const addClass = function (id: string) {
const classId = splitClassNameAndType(id);
const { className, type } = splitClassNameAndType(id);
// Only add class if not exists
if (classes[classId.className] !== undefined) {
if (Object.hasOwn(classes, className)) {
return;
}
classes[classId.className] = {
id: classId.className,
type: classId.type,
label: classId.className,
classes[className] = {
id: className,
type: type,
label: className,
cssClasses: [],
methods: [],
members: [],
annotations: [],
domId: MERMAID_DOM_ID_PREFIX + classId.className + '-' + classCounter,
domId: MERMAID_DOM_ID_PREFIX + className + '-' + classCounter,
} as ClassNode;
classCounter++;
@@ -114,11 +109,11 @@ export const clear = function () {
commonClear();
};
export const getClass = function (id: string) {
export const getClass = function (id: string): ClassNode {
return classes[id];
};
export const getClasses = function () {
export const getClasses = function (): ClassMap {
return classes;
};
@@ -174,6 +169,8 @@ export const addAnnotation = function (className: string, annotation: string) {
* @public
*/
export const addMember = function (className: string, member: string) {
addClass(className);
const validatedClassName = splitClassNameAndType(className).className;
const theClass = classes[validatedClassName];
@@ -186,9 +183,9 @@ export const addMember = function (className: string, member: string) {
theClass.annotations.push(sanitizeText(memberString.substring(2, memberString.length - 2)));
} else if (memberString.indexOf(')') > 0) {
//its a method
theClass.methods.push(sanitizeText(memberString));
theClass.methods.push(new ClassMember(memberString, 'method'));
} else if (memberString) {
theClass.members.push(sanitizeText(memberString));
theClass.members.push(new ClassMember(memberString, 'attribute'));
}
}
};
@@ -255,6 +252,7 @@ export const getTooltip = function (id: string, namespace?: string) {
return classes[id].tooltip;
};
/**
* Called by parser when a link is found. Adds the URL to the vertex data.
*
@@ -367,6 +365,7 @@ export const relationType = {
const setupToolTips = function (element: Element) {
let tooltipElem: Selection<HTMLDivElement, unknown, HTMLElement, unknown> =
select('.mermaidTooltip');
// @ts-expect-error - Incorrect types
if ((tooltipElem._groups || tooltipElem)[0][0] === null) {
tooltipElem = select('body').append('div').attr('class', 'mermaidTooltip').style('opacity', 0);
}
@@ -376,7 +375,6 @@ const setupToolTips = function (element: Element) {
const nodes = svg.selectAll('g.node');
nodes
.on('mouseover', function () {
// @ts-expect-error - select is not part of the d3 type definition
const el = select(this);
const title = el.attr('title');
// Don't try to draw a tooltip if no data is provided
@@ -386,6 +384,7 @@ const setupToolTips = function (element: Element) {
// @ts-ignore - getBoundingClientRect is not part of the d3 type definition
const rect = this.getBoundingClientRect();
// @ts-expect-error - Incorrect types
tooltipElem.transition().duration(200).style('opacity', '.9');
tooltipElem
.text(el.attr('title'))
@@ -395,8 +394,8 @@ const setupToolTips = function (element: Element) {
el.classed('hover', true);
})
.on('mouseout', function () {
// @ts-expect-error - Incorrect types
tooltipElem.transition().duration(500).style('opacity', 0);
// @ts-expect-error - select is not part of the d3 type definition
const el = select(this);
el.classed('hover', false);
});
@@ -455,7 +454,6 @@ export const addClassesToNamespace = function (id: string, classNames: string[])
};
export default {
parseDirective,
setAccTitle,
getAccTitle,
getAccDescription,

View File

@@ -1,4 +1,4 @@
import { DiagramDefinition } from '../../diagram-api/types.js';
import type { DiagramDefinition } from '../../diagram-api/types.js';
// @ts-ignore: JISON doesn't support types
import parser from './parser/classDiagram.jison';
import db from './classDb.js';

View File

@@ -4,6 +4,9 @@ import classDb from './classDb.js';
import { vi, describe, it, expect } from 'vitest';
const spyOn = vi.spyOn;
const staticCssStyle = 'text-decoration:underline;';
const abstractCssStyle = 'font-style:italic;';
describe('given a basic class diagram, ', function () {
describe('when parsing class definition', function () {
beforeEach(function () {
@@ -127,7 +130,7 @@ describe('given a basic class diagram, ', function () {
const c1 = classDb.getClass('C1');
expect(c1.label).toBe('Class 1 with text label');
expect(c1.members.length).toBe(1);
expect(c1.members[0]).toBe('member1');
expect(c1.members[0].getDisplayDetails().displayText).toBe('member1');
});
it('should parse a class with a text label, member and annotation', () => {
@@ -142,7 +145,7 @@ describe('given a basic class diagram, ', function () {
const c1 = classDb.getClass('C1');
expect(c1.label).toBe('Class 1 with text label');
expect(c1.members.length).toBe(1);
expect(c1.members[0]).toBe('int member1');
expect(c1.members[0].getDisplayDetails().displayText).toBe('int member1');
expect(c1.annotations.length).toBe(1);
expect(c1.annotations[0]).toBe('interface');
});
@@ -168,7 +171,7 @@ describe('given a basic class diagram, ', function () {
const c1 = classDb.getClass('C1');
expect(c1.label).toBe('Class 1 with text label');
expect(c1.members[0]).toBe('int member1');
expect(c1.members[0].getDisplayDetails().displayText).toBe('int member1');
expect(c1.cssClasses[0]).toBe('styleClass');
});
@@ -371,6 +374,74 @@ class C13["With Città foreign language"]
note ${keyword}`;
expect(() => parser.parse(str)).toThrowError(/(Expecting\s'STR'|Unrecognized\stext)/);
});
it('should parse diagram with direction', () => {
parser.parse(`classDiagram
direction TB
class Student {
-idCard : IdCard
}
class IdCard{
-id : int
-name : string
}
class Bike{
-id : int
-name : string
}
Student "1" --o "1" IdCard : carries
Student "1" --o "1" Bike : rides`);
expect(Object.keys(classDb.getClasses()).length).toBe(3);
expect(classDb.getClasses().Student).toMatchInlineSnapshot(`
{
"annotations": [],
"cssClasses": [],
"domId": "classId-Student-134",
"id": "Student",
"label": "Student",
"members": [
ClassMember {
"classifier": "",
"id": "idCard : IdCard",
"memberType": "attribute",
"visibility": "-",
},
],
"methods": [],
"type": "",
}
`);
expect(classDb.getRelations().length).toBe(2);
expect(classDb.getRelations()).toMatchInlineSnapshot(`
[
{
"id1": "Student",
"id2": "IdCard",
"relation": {
"lineType": 0,
"type1": "none",
"type2": 0,
},
"relationTitle1": "1",
"relationTitle2": "1",
"title": "carries",
},
{
"id1": "Student",
"id2": "Bike",
"relation": {
"lineType": 0,
"type1": "none",
"type2": 0,
},
"relationTitle1": "1",
"relationTitle2": "1",
"title": "rides",
},
]
`);
});
});
describe('when parsing class defined in brackets', function () {
@@ -422,8 +493,8 @@ class C13["With Città foreign language"]
'classDiagram\n' +
'class Class1 {\n' +
'int testMember\n' +
'string fooMember\n' +
'test()\n' +
'string fooMember\n' +
'foo()\n' +
'}';
parser.parse(str);
@@ -431,10 +502,10 @@ class C13["With Città foreign language"]
const actual = parser.yy.getClass('Class1');
expect(actual.members.length).toBe(2);
expect(actual.methods.length).toBe(2);
expect(actual.members[0]).toBe('int testMember');
expect(actual.members[1]).toBe('string fooMember');
expect(actual.methods[0]).toBe('test()');
expect(actual.methods[1]).toBe('foo()');
expect(actual.members[0].getDisplayDetails().displayText).toBe('int testMember');
expect(actual.members[1].getDisplayDetails().displayText).toBe('string fooMember');
expect(actual.methods[0].getDisplayDetails().displayText).toBe('test()');
expect(actual.methods[1].getDisplayDetails().displayText).toBe('foo()');
});
it('should parse a class with a text label and members', () => {
@@ -444,7 +515,7 @@ class C13["With Città foreign language"]
const c1 = classDb.getClass('C1');
expect(c1.label).toBe('Class 1 with text label');
expect(c1.members.length).toBe(1);
expect(c1.members[0]).toBe('+member1');
expect(c1.members[0].getDisplayDetails().displayText).toBe('+member1');
});
it('should parse a class with a text label, members and annotation', () => {
@@ -459,7 +530,7 @@ class C13["With Città foreign language"]
const c1 = classDb.getClass('C1');
expect(c1.label).toBe('Class 1 with text label');
expect(c1.members.length).toBe(1);
expect(c1.members[0]).toBe('+member1');
expect(c1.members[0].getDisplayDetails().displayText).toBe('+member1');
expect(c1.annotations.length).toBe(1);
expect(c1.annotations[0]).toBe('interface');
});
@@ -742,6 +813,20 @@ describe('given a class diagram with members and methods ', function () {
parser.parse(str);
});
it('should handle direct member declaration', function () {
parser.parse('classDiagram\n' + 'Car : wheels');
const car = classDb.getClass('Car');
expect(car.members.length).toBe(1);
expect(car.members[0].id).toBe('wheels');
});
it('should handle direct member declaration with type', function () {
parser.parse('classDiagram\n' + 'Car : int wheels');
const car = classDb.getClass('Car');
expect(car.members.length).toBe(1);
expect(car.members[0].id).toBe('int wheels');
});
it('should handle simple member declaration with type', function () {
const str = 'classDiagram\n' + 'class Car\n' + 'Car : int wheels';
@@ -762,10 +847,10 @@ describe('given a class diagram with members and methods ', function () {
const actual = parser.yy.getClass('actual');
expect(actual.members.length).toBe(4);
expect(actual.methods.length).toBe(0);
expect(actual.members[0]).toBe('-int privateMember');
expect(actual.members[1]).toBe('+int publicMember');
expect(actual.members[2]).toBe('#int protectedMember');
expect(actual.members[3]).toBe('~int privatePackage');
expect(actual.members[0].getDisplayDetails().displayText).toBe('-int privateMember');
expect(actual.members[1].getDisplayDetails().displayText).toBe('+int publicMember');
expect(actual.members[2].getDisplayDetails().displayText).toBe('#int protectedMember');
expect(actual.members[3].getDisplayDetails().displayText).toBe('~int privatePackage');
});
it('should handle generic types', function () {
@@ -818,7 +903,9 @@ describe('given a class diagram with members and methods ', function () {
expect(actual.annotations.length).toBe(0);
expect(actual.members.length).toBe(0);
expect(actual.methods.length).toBe(1);
expect(actual.methods[0]).toBe('someMethod()*');
const method = actual.methods[0];
expect(method.getDisplayDetails().displayText).toBe('someMethod()');
expect(method.getDisplayDetails().cssStyle).toBe(abstractCssStyle);
});
it('should handle static methods', function () {
@@ -829,7 +916,9 @@ describe('given a class diagram with members and methods ', function () {
expect(actual.annotations.length).toBe(0);
expect(actual.members.length).toBe(0);
expect(actual.methods.length).toBe(1);
expect(actual.methods[0]).toBe('someMethod()$');
const method = actual.methods[0];
expect(method.getDisplayDetails().displayText).toBe('someMethod()');
expect(method.getDisplayDetails().cssStyle).toBe(staticCssStyle);
});
it('should handle generic types in arguments', function () {
@@ -955,53 +1044,6 @@ foo()
parser.parse(str);
});
});
describe('when parsing invalid generic classes', function () {
beforeEach(function () {
classDb.clear();
parser.yy = classDb;
});
it('should break when another `{`is encountered before closing the first one while defining generic class with brackets', function () {
const str =
'classDiagram\n' +
'class Dummy_Class~T~ {\n' +
'String data\n' +
' void methods()\n' +
'}\n' +
'\n' +
'class Dummy_Class {\n' +
'class Flight {\n' +
' flightNumber : Integer\n' +
' departureTime : Date\n' +
'}';
let testPassed = false;
try {
parser.parse(str);
} catch (error) {
testPassed = true;
}
expect(testPassed).toBe(true);
});
it('should break when EOF is encountered before closing the first `{` while defining generic class with brackets', function () {
const str =
'classDiagram\n' +
'class Dummy_Class~T~ {\n' +
'String data\n' +
' void methods()\n' +
'}\n' +
'\n' +
'class Dummy_Class {\n';
let testPassed = false;
try {
parser.parse(str);
} catch (error) {
testPassed = true;
}
expect(testPassed).toBe(true);
});
});
});
describe('given a class diagram with relationships, ', function () {
@@ -1274,10 +1316,10 @@ describe('given a class diagram with relationships, ', function () {
const testClass = parser.yy.getClass('Class1');
expect(testClass.members.length).toBe(2);
expect(testClass.methods.length).toBe(2);
expect(testClass.members[0]).toBe('int : test');
expect(testClass.members[1]).toBe('string : foo');
expect(testClass.methods[0]).toBe('test()');
expect(testClass.methods[1]).toBe('foo()');
expect(testClass.members[0].getDisplayDetails().displayText).toBe('int : test');
expect(testClass.members[1].getDisplayDetails().displayText).toBe('string : foo');
expect(testClass.methods[0].getDisplayDetails().displayText).toBe('test()');
expect(testClass.methods[1].getDisplayDetails().displayText).toBe('foo()');
});
it('should handle abstract methods', function () {
@@ -1288,7 +1330,9 @@ describe('given a class diagram with relationships, ', function () {
expect(testClass.annotations.length).toBe(0);
expect(testClass.members.length).toBe(0);
expect(testClass.methods.length).toBe(1);
expect(testClass.methods[0]).toBe('someMethod()*');
const method = testClass.methods[0];
expect(method.getDisplayDetails().displayText).toBe('someMethod()');
expect(method.getDisplayDetails().cssStyle).toBe(abstractCssStyle);
});
it('should handle static methods', function () {
@@ -1299,7 +1343,9 @@ describe('given a class diagram with relationships, ', function () {
expect(testClass.annotations.length).toBe(0);
expect(testClass.members.length).toBe(0);
expect(testClass.methods.length).toBe(1);
expect(testClass.methods[0]).toBe('someMethod()$');
const method = testClass.methods[0];
expect(method.getDisplayDetails().displayText).toBe('someMethod()');
expect(method.getDisplayDetails().cssStyle).toBe(staticCssStyle);
});
it('should associate link and css appropriately', function () {
@@ -1514,11 +1560,19 @@ class Class2
const testClasses = parser.yy.getClasses();
const testRelations = parser.yy.getRelations();
expect(Object.keys(testNamespaceA.classes).length).toBe(2);
expect(testNamespaceA.classes['A1'].members[0]).toBe('+foo : string');
expect(testNamespaceA.classes['A2'].members[0]).toBe('+bar : int');
expect(testNamespaceA.classes['A1'].members[0].getDisplayDetails().displayText).toBe(
'+foo : string'
);
expect(testNamespaceA.classes['A2'].members[0].getDisplayDetails().displayText).toBe(
'+bar : int'
);
expect(Object.keys(testNamespaceB.classes).length).toBe(2);
expect(testNamespaceB.classes['B1'].members[0]).toBe('+foo : bool');
expect(testNamespaceB.classes['B2'].members[0]).toBe('+bar : float');
expect(testNamespaceB.classes['B1'].members[0].getDisplayDetails().displayText).toBe(
'+foo : bool'
);
expect(testNamespaceB.classes['B2'].members[0].getDisplayDetails().displayText).toBe(
'+bar : float'
);
expect(Object.keys(testClasses).length).toBe(4);
expect(testClasses['A1'].parent).toBe('A');
expect(testClasses['A2'].parent).toBe('A');
@@ -1570,8 +1624,8 @@ class Class2
const c1 = classDb.getClass('C1');
expect(c1.label).toBe('Class 1 with text label');
expect(c1.members.length).toBe(1);
expect(c1.members[0]).toBe('+member1');
const member = c1.members[0];
expect(member.getDisplayDetails().displayText).toBe('+member1');
const c2 = classDb.getClass('C2');
expect(c2.label).toBe('C2');
});
@@ -1587,9 +1641,10 @@ class Class2
const c1 = classDb.getClass('C1');
expect(c1.label).toBe('Class 1 with text label');
expect(c1.members.length).toBe(1);
expect(c1.members[0]).toBe('+member1');
expect(c1.annotations.length).toBe(1);
expect(c1.annotations[0]).toBe('interface');
const member = c1.members[0];
expect(member.getDisplayDetails().displayText).toBe('+member1');
const c2 = classDb.getClass('C2');
expect(c2.label).toBe('C2');
@@ -1606,8 +1661,9 @@ C1 --> C2
const c1 = classDb.getClass('C1');
expect(c1.label).toBe('Class 1 with text label');
expect(c1.cssClasses.length).toBe(1);
expect(c1.members[0]).toBe('+member1');
expect(c1.cssClasses[0]).toBe('styleClass');
const member = c1.members[0];
expect(member.getDisplayDetails().displayText).toBe('+member1');
});
it('should parse a class with text label and css class', () => {
@@ -1622,8 +1678,9 @@ cssClass "C1" styleClass
const c1 = classDb.getClass('C1');
expect(c1.label).toBe('Class 1 with text label');
expect(c1.cssClasses.length).toBe(1);
expect(c1.members[0]).toBe('+member1');
expect(c1.cssClasses[0]).toBe('styleClass');
const member = c1.members[0];
expect(member.getDisplayDetails().displayText).toBe('+member1');
});
it('should parse two classes with text labels and css classes', () => {

View File

@@ -1,4 +1,4 @@
import { DiagramDefinition } from '../../diagram-api/types.js';
import type { DiagramDefinition } from '../../diagram-api/types.js';
// @ts-ignore: JISON doesn't support types
import parser from './parser/classDiagram.jison';
import db from './classDb.js';

View File

@@ -1,78 +0,0 @@
import { setConfig } from '../../config.js';
import classDB from './classDb.js';
// @ts-ignore - no types in jison
import classDiagram from './parser/classDiagram.jison';
setConfig({
securityLevel: 'strict',
});
describe('when parsing class diagram', function () {
beforeEach(function () {
classDiagram.parser.yy = classDB;
classDiagram.parser.yy.clear();
});
it('should parse diagram with direction', () => {
classDiagram.parser.parse(`classDiagram
direction TB
class Student {
-idCard : IdCard
}
class IdCard{
-id : int
-name : string
}
class Bike{
-id : int
-name : string
}
Student "1" --o "1" IdCard : carries
Student "1" --o "1" Bike : rides`);
expect(Object.keys(classDB.getClasses()).length).toBe(3);
expect(classDB.getClasses().Student).toMatchInlineSnapshot(`
{
"annotations": [],
"cssClasses": [],
"domId": "classId-Student-0",
"id": "Student",
"label": "Student",
"members": [
"-idCard : IdCard",
],
"methods": [],
"type": "",
}
`);
expect(classDB.getRelations().length).toBe(2);
expect(classDB.getRelations()).toMatchInlineSnapshot(`
[
{
"id1": "Student",
"id2": "IdCard",
"relation": {
"lineType": 0,
"type1": "none",
"type2": 0,
},
"relationTitle1": "1",
"relationTitle2": "1",
"title": "carries",
},
{
"id1": "Student",
"id2": "Bike",
"relation": {
"lineType": 0,
"type1": "none",
"type2": 0,
},
"relationTitle1": "1",
"relationTitle2": "1",
"title": "rides",
},
]
`);
});
});

View File

@@ -8,7 +8,7 @@ import utils from '../../utils.js';
import { interpolateToCurve, getStylesFromArray } from '../../utils.js';
import { setupGraphViewbox } from '../../setupGraphViewbox.js';
import common from '../common/common.js';
import { ClassRelation, ClassNote, ClassMap, EdgeData, NamespaceMap } from './classTypes.js';
import type { ClassRelation, ClassNote, ClassMap, EdgeData, NamespaceMap } from './classTypes.js';
const sanitizeText = (txt: string) => common.sanitizeText(txt, getConfig());
@@ -156,24 +156,17 @@ export const addNotes = function (
) {
log.info(notes);
// Iterate through each item in the vertex object (containing all the vertices found) in the graph definition
notes.forEach(function (note, i) {
const vertex = note;
/**
* Variable for storing the classes for the vertex
*
*/
const cssNoteStr = '';
const styles = { labelStyle: '', style: '' };
// Use vertex id as text in the box if no text is provided by the graph definition
const vertexText = vertex.text;
const radius = 0;
const shape = 'note';
// Add the node
const node = {
labelStyle: styles.labelStyle,
shape: shape,
@@ -301,7 +294,7 @@ export const setConf = function (cnf: any) {
};
/**
* Draws a flowchart in the tag with id: id based on the graph definition in text.
* Draws a class diagram in the tag with id: id based on the definition in text.
*
* @param text -
* @param id -

View File

@@ -0,0 +1,683 @@
import { ClassMember } from './classTypes.js';
import { vi, describe, it, expect } from 'vitest';
const spyOn = vi.spyOn;
const staticCssStyle = 'text-decoration:underline;';
const abstractCssStyle = 'font-style:italic;';
describe('given text representing a method, ', function () {
describe('when method has no parameters', function () {
it('should parse correctly', function () {
const str = `getTime()`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe(str);
});
it('should handle public visibility', function () {
const str = `+getTime()`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('+getTime()');
});
it('should handle private visibility', function () {
const str = `-getTime()`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('-getTime()');
});
it('should handle protected visibility', function () {
const str = `#getTime()`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('#getTime()');
});
it('should handle internal visibility', function () {
const str = `~getTime()`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('~getTime()');
});
it('should return correct css for static classifier', function () {
const str = `getTime()$`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('getTime()');
expect(classMember.getDisplayDetails().cssStyle).toBe(staticCssStyle);
});
it('should return correct css for abstract classifier', function () {
const str = `getTime()*`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('getTime()');
expect(classMember.getDisplayDetails().cssStyle).toBe(abstractCssStyle);
});
});
describe('when method has single parameter value', function () {
it('should parse correctly', function () {
const str = `getTime(int)`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe(str);
});
it('should handle public visibility', function () {
const str = `+getTime(int)`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('+getTime(int)');
});
it('should handle private visibility', function () {
const str = `-getTime(int)`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('-getTime(int)');
});
it('should handle protected visibility', function () {
const str = `#getTime(int)`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('#getTime(int)');
});
it('should handle internal visibility', function () {
const str = `~getTime(int)`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('~getTime(int)');
});
it('should return correct css for static classifier', function () {
const str = `getTime(int)$`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('getTime(int)');
expect(classMember.getDisplayDetails().cssStyle).toBe(staticCssStyle);
});
it('should return correct css for abstract classifier', function () {
const str = `getTime(int)*`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('getTime(int)');
expect(classMember.getDisplayDetails().cssStyle).toBe(abstractCssStyle);
});
});
describe('when method has single parameter type and name (type first)', function () {
it('should parse correctly', function () {
const str = `getTime(int count)`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe(str);
});
it('should handle public visibility', function () {
const str = `+getTime(int count)`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('+getTime(int count)');
});
it('should handle private visibility', function () {
const str = `-getTime(int count)`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('-getTime(int count)');
});
it('should handle protected visibility', function () {
const str = `#getTime(int count)`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('#getTime(int count)');
});
it('should handle internal visibility', function () {
const str = `~getTime(int count)`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('~getTime(int count)');
});
it('should return correct css for static classifier', function () {
const str = `getTime(int count)$`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('getTime(int count)');
expect(classMember.getDisplayDetails().cssStyle).toBe(staticCssStyle);
});
it('should return correct css for abstract classifier', function () {
const str = `getTime(int count)*`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('getTime(int count)');
expect(classMember.getDisplayDetails().cssStyle).toBe(abstractCssStyle);
});
});
describe('when method has single parameter type and name (name first)', function () {
it('should parse correctly', function () {
const str = `getTime(count int)`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe(str);
});
it('should handle public visibility', function () {
const str = `+getTime(count int)`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('+getTime(count int)');
});
it('should handle private visibility', function () {
const str = `-getTime(count int)`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('-getTime(count int)');
});
it('should handle protected visibility', function () {
const str = `#getTime(count int)`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('#getTime(count int)');
});
it('should handle internal visibility', function () {
const str = `~getTime(count int)`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('~getTime(count int)');
});
it('should return correct css for static classifier', function () {
const str = `getTime(count int)$`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('getTime(count int)');
expect(classMember.getDisplayDetails().cssStyle).toBe(staticCssStyle);
});
it('should return correct css for abstract classifier', function () {
const str = `getTime(count int)*`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('getTime(count int)');
expect(classMember.getDisplayDetails().cssStyle).toBe(abstractCssStyle);
});
});
describe('when method has multiple parameters', function () {
it('should parse correctly', function () {
const str = `getTime(string text, int count)`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe(str);
});
it('should handle public visibility', function () {
const str = `+getTime(string text, int count)`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe(str);
});
it('should handle private visibility', function () {
const str = `-getTime(string text, int count)`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe(str);
});
it('should handle protected visibility', function () {
const str = `#getTime(string text, int count)`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe(str);
});
it('should handle internal visibility', function () {
const str = `~getTime(string text, int count)`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe(str);
});
it('should return correct css for static classifier', function () {
const str = `getTime(string text, int count)$`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('getTime(string text, int count)');
expect(classMember.getDisplayDetails().cssStyle).toBe(staticCssStyle);
});
it('should return correct css for abstract classifier', function () {
const str = `getTime(string text, int count)*`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('getTime(string text, int count)');
expect(classMember.getDisplayDetails().cssStyle).toBe(abstractCssStyle);
});
});
describe('when method has return type', function () {
it('should parse correctly', function () {
const str = `getTime() DateTime`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('getTime() : DateTime');
});
it('should handle public visibility', function () {
const str = `+getTime() DateTime`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('+getTime() : DateTime');
});
it('should handle private visibility', function () {
const str = `-getTime() DateTime`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('-getTime() : DateTime');
});
it('should handle protected visibility', function () {
const str = `#getTime() DateTime`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('#getTime() : DateTime');
});
it('should handle internal visibility', function () {
const str = `~getTime() DateTime`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('~getTime() : DateTime');
});
it('should return correct css for static classifier', function () {
const str = `getTime() DateTime$`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('getTime() : DateTime');
expect(classMember.getDisplayDetails().cssStyle).toBe(staticCssStyle);
});
it('should return correct css for abstract classifier', function () {
const str = `getTime() DateTime*`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('getTime() : DateTime');
expect(classMember.getDisplayDetails().cssStyle).toBe(abstractCssStyle);
});
});
describe('when method parameter is generic', function () {
it('should parse correctly', function () {
const str = `getTimes(List~T~)`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('getTimes(List<T>)');
});
it('should handle public visibility', function () {
const str = `+getTimes(List~T~)`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('+getTimes(List<T>)');
});
it('should handle private visibility', function () {
const str = `-getTimes(List~T~)`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('-getTimes(List<T>)');
});
it('should handle protected visibility', function () {
const str = `#getTimes(List~T~)`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('#getTimes(List<T>)');
});
it('should handle internal visibility', function () {
const str = `~getTimes(List~T~)`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('~getTimes(List<T>)');
});
it('should return correct css for static classifier', function () {
const str = `getTimes(List~T~)$`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('getTimes(List<T>)');
expect(classMember.getDisplayDetails().cssStyle).toBe(staticCssStyle);
});
it('should return correct css for abstract classifier', function () {
const str = `getTimes(List~T~)*`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('getTimes(List<T>)');
expect(classMember.getDisplayDetails().cssStyle).toBe(abstractCssStyle);
});
});
describe('when method parameter contains two generic', function () {
it('should parse correctly', function () {
const str = `getTimes(List~T~, List~OT~)`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('getTimes(List<T>, List<OT>)');
});
it('should handle public visibility', function () {
const str = `+getTimes(List~T~, List~OT~)`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('+getTimes(List<T>, List<OT>)');
});
it('should handle private visibility', function () {
const str = `-getTimes(List~T~, List~OT~)`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('-getTimes(List<T>, List<OT>)');
});
it('should handle protected visibility', function () {
const str = `#getTimes(List~T~, List~OT~)`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('#getTimes(List<T>, List<OT>)');
});
it('should handle internal visibility', function () {
const str = `~getTimes(List~T~, List~OT~)`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('~getTimes(List<T>, List<OT>)');
});
it('should return correct css for static classifier', function () {
const str = `getTimes(List~T~, List~OT~)$`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('getTimes(List<T>, List<OT>)');
expect(classMember.getDisplayDetails().cssStyle).toBe(staticCssStyle);
});
it('should return correct css for abstract classifier', function () {
const str = `getTimes(List~T~, List~OT~)*`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('getTimes(List<T>, List<OT>)');
expect(classMember.getDisplayDetails().cssStyle).toBe(abstractCssStyle);
});
});
describe('when method parameter is a nested generic', function () {
it('should parse correctly', function () {
const str = `getTimetableList(List~List~T~~)`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('getTimetableList(List<List<T>>)');
});
it('should handle public visibility', function () {
const str = `+getTimetableList(List~List~T~~)`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('+getTimetableList(List<List<T>>)');
});
it('should handle private visibility', function () {
const str = `-getTimetableList(List~List~T~~)`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('-getTimetableList(List<List<T>>)');
});
it('should handle protected visibility', function () {
const str = `#getTimetableList(List~List~T~~)`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('#getTimetableList(List<List<T>>)');
});
it('should handle internal visibility', function () {
const str = `~getTimetableList(List~List~T~~)`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('~getTimetableList(List<List<T>>)');
});
it('should return correct css for static classifier', function () {
const str = `getTimetableList(List~List~T~~)$`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('getTimetableList(List<List<T>>)');
expect(classMember.getDisplayDetails().cssStyle).toBe(staticCssStyle);
});
it('should return correct css for abstract classifier', function () {
const str = `getTimetableList(List~List~T~~)*`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('getTimetableList(List<List<T>>)');
expect(classMember.getDisplayDetails().cssStyle).toBe(abstractCssStyle);
});
});
describe('when method parameter is a composite generic', function () {
const methodNameAndParameters = 'getTimes(List~K, V~)';
const expectedMethodNameAndParameters = 'getTimes(List<K, V>)';
it('should parse correctly', function () {
const str = methodNameAndParameters;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe(expectedMethodNameAndParameters);
});
it('should handle public visibility', function () {
const str = '+' + methodNameAndParameters;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe(
'+' + expectedMethodNameAndParameters
);
});
it('should handle private visibility', function () {
const str = '-' + methodNameAndParameters;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe(
'-' + expectedMethodNameAndParameters
);
});
it('should handle protected visibility', function () {
const str = '#' + methodNameAndParameters;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe(
'#' + expectedMethodNameAndParameters
);
});
it('should handle internal visibility', function () {
const str = '~' + methodNameAndParameters;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe(
'~' + expectedMethodNameAndParameters
);
});
it('should return correct css for static classifier', function () {
const str = methodNameAndParameters + '$';
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe(expectedMethodNameAndParameters);
expect(classMember.getDisplayDetails().cssStyle).toBe(staticCssStyle);
});
it('should return correct css for abstract classifier', function () {
const str = methodNameAndParameters + '*';
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe(expectedMethodNameAndParameters);
expect(classMember.getDisplayDetails().cssStyle).toBe(abstractCssStyle);
});
});
describe('when method return type is generic', function () {
it('should parse correctly', function () {
const str = `getTimes() List~T~`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('getTimes() : List<T>');
});
it('should handle public visibility', function () {
const str = `+getTimes() List~T~`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('+getTimes() : List<T>');
});
it('should handle private visibility', function () {
const str = `-getTimes() List~T~`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('-getTimes() : List<T>');
});
it('should handle protected visibility', function () {
const str = `#getTimes() List~T~`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('#getTimes() : List<T>');
});
it('should handle internal visibility', function () {
const str = `~getTimes() List~T~`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('~getTimes() : List<T>');
});
it('should return correct css for static classifier', function () {
const str = `getTimes() List~T~$`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('getTimes() : List<T>');
expect(classMember.getDisplayDetails().cssStyle).toBe(staticCssStyle);
});
it('should return correct css for abstract classifier', function () {
const str = `getTimes() List~T~*`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe('getTimes() : List<T>');
expect(classMember.getDisplayDetails().cssStyle).toBe(abstractCssStyle);
});
});
describe('when method return type is a nested generic', function () {
it('should parse correctly', function () {
const str = `getTimetableList() List~List~T~~`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe(
'getTimetableList() : List<List<T>>'
);
});
it('should handle public visibility', function () {
const str = `+getTimetableList() List~List~T~~`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe(
'+getTimetableList() : List<List<T>>'
);
});
it('should handle private visibility', function () {
const str = `-getTimetableList() List~List~T~~`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe(
'-getTimetableList() : List<List<T>>'
);
});
it('should handle protected visibility', function () {
const str = `#getTimetableList() List~List~T~~`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe(
'#getTimetableList() : List<List<T>>'
);
});
it('should handle internal visibility', function () {
const str = `~getTimetableList() List~List~T~~`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe(
'~getTimetableList() : List<List<T>>'
);
});
it('should return correct css for static classifier', function () {
const str = `getTimetableList() List~List~T~~$`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe(
'getTimetableList() : List<List<T>>'
);
expect(classMember.getDisplayDetails().cssStyle).toBe(staticCssStyle);
});
it('should return correct css for abstract classifier', function () {
const str = `getTimetableList() List~List~T~~*`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe(
'getTimetableList() : List<List<T>>'
);
expect(classMember.getDisplayDetails().cssStyle).toBe(abstractCssStyle);
});
});
describe('--uncategorized tests--', function () {
it('member name should handle double colons', function () {
const str = `std::map ~int,string~ pMap;`;
const classMember = new ClassMember(str, 'attribute');
expect(classMember.getDisplayDetails().displayText).toBe('std::map <int,string> pMap;');
});
it('member name should handle generic type', function () {
const str = `getTime~T~(this T, int seconds)$ DateTime`;
const classMember = new ClassMember(str, 'method');
expect(classMember.getDisplayDetails().displayText).toBe(
'getTime<T>(this T, int seconds) : DateTime'
);
expect(classMember.getDisplayDetails().cssStyle).toBe(staticCssStyle);
});
});
});

View File

@@ -1,10 +1,13 @@
import { getConfig } from '../../config.js';
import { parseGenericTypes, sanitizeText } from '../common/common.js';
export interface ClassNode {
id: string;
type: string;
label: string;
cssClasses: string[];
methods: string[];
members: string[];
methods: ClassMember[];
members: ClassMember[];
annotations: string[];
domId: string;
parent?: string;
@@ -14,6 +17,120 @@ export interface ClassNode {
tooltip?: string;
}
export type Visibility = '#' | '+' | '~' | '-' | '';
export const visibilityValues = ['#', '+', '~', '-', ''];
/**
* Parses and stores class diagram member variables/methods.
*
*/
export class ClassMember {
id!: string;
cssStyle!: string;
memberType!: 'method' | 'attribute';
visibility!: Visibility;
/**
* denote if static or to determine which css class to apply to the node
* @defaultValue ''
*/
classifier!: string;
/**
* parameters for method
* @defaultValue ''
*/
parameters!: string;
/**
* return type for method
* @defaultValue ''
*/
returnType!: string;
constructor(input: string, memberType: 'method' | 'attribute') {
this.memberType = memberType;
this.visibility = '';
this.classifier = '';
const sanitizedInput = sanitizeText(input, getConfig());
this.parseMember(sanitizedInput);
}
getDisplayDetails() {
let displayText = this.visibility + parseGenericTypes(this.id);
if (this.memberType === 'method') {
displayText += `(${parseGenericTypes(this.parameters.trim())})`;
if (this.returnType) {
displayText += ' : ' + parseGenericTypes(this.returnType);
}
}
displayText = displayText.trim();
const cssStyle = this.parseClassifier();
return {
displayText,
cssStyle,
};
}
parseMember(input: string) {
let potentialClassifier = '';
if (this.memberType === 'method') {
const methodRegEx = /([#+~-])?(.+)\((.*)\)([\s$*])?(.*)([$*])?/;
const match = input.match(methodRegEx);
if (match) {
const detectedVisibility = match[1] ? match[1].trim() : '';
if (visibilityValues.includes(detectedVisibility)) {
this.visibility = detectedVisibility as Visibility;
}
this.id = match[2].trim();
this.parameters = match[3] ? match[3].trim() : '';
potentialClassifier = match[4] ? match[4].trim() : '';
this.returnType = match[5] ? match[5].trim() : '';
if (potentialClassifier === '') {
const lastChar = this.returnType.substring(this.returnType.length - 1);
if (lastChar.match(/[$*]/)) {
potentialClassifier = lastChar;
this.returnType = this.returnType.substring(0, this.returnType.length - 1);
}
}
}
} else {
const length = input.length;
const firstChar = input.substring(0, 1);
const lastChar = input.substring(length - 1);
if (visibilityValues.includes(firstChar)) {
this.visibility = firstChar as Visibility;
}
if (lastChar.match(/[*?]/)) {
potentialClassifier = lastChar;
}
this.id = input.substring(
this.visibility === '' ? 0 : 1,
potentialClassifier === '' ? length : length - 1
);
}
this.classifier = potentialClassifier;
}
parseClassifier() {
switch (this.classifier) {
case '*':
return 'font-style:italic;';
case '$':
return 'text-decoration:underline;';
default:
return '';
}
}
}
export interface ClassNote {
id: string;
class: string;

View File

@@ -13,9 +13,6 @@
%x href
%x callback_name
%x callback_args
%x open_directive
%x type_directive
%x arg_directive
%x acc_title
%x acc_descr
%x acc_descr_multiline
@@ -24,15 +21,10 @@
%x namespace
%x namespace-body
%%
\%\%\{ { this.begin('open_directive'); return 'open_directive'; }
.*direction\s+TB[^\n]* return 'direction_tb';
.*direction\s+BT[^\n]* return 'direction_bt';
.*direction\s+RL[^\n]* return 'direction_rl';
.*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 */
accTitle\s*":"\s* { this.begin("acc_title");return 'acc_title'; }
@@ -220,46 +212,13 @@ line was introduced with 'click'.
start
: mermaidDoc
| directive start
| statements
;
direction
: direction_tb
{ yy.setDirection('TB');}
| direction_bt
{ yy.setDirection('BT');}
| direction_rl
{ yy.setDirection('RL');}
| direction_lr
{ yy.setDirection('LR');}
;
mermaidDoc
: graphConfig
;
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'); }
;
graphConfig
: CLASS_DIAGRAM NEWLINE statements EOF
;
@@ -292,7 +251,7 @@ statement
| relationStatement LABEL { $1.title = yy.cleanupLabel($2); yy.addRelation($1); }
| namespaceStatement
| classStatement
| methodStatement
| memberStatement
| annotationStatement
| clickStatement
| cssClassStatement
@@ -339,7 +298,7 @@ members
| MEMBER members { $2.push($1);$$=$2;}
;
methodStatement
memberStatement
: className {/*console.log('Rel found',$1);*/}
| className LABEL {yy.addMember($1,yy.cleanupLabel($2));}
| MEMBER {/*console.warn('Member',$1);*/}
@@ -358,6 +317,17 @@ noteStatement
| NOTE noteText { yy.addNote($2); }
;
direction
: direction_tb
{ yy.setDirection('TB');}
| direction_bt
{ yy.setDirection('BT');}
| direction_rl
{ yy.setDirection('RL');}
| direction_lr
{ yy.setDirection('LR');}
;
relation
: relationType lineType relationType { $$={type1:$1,type2:$3,lineType:$2}; }
| lineType relationType { $$={type1:'none',type2:$2,lineType:$1}; }

View File

@@ -109,25 +109,25 @@ g.classGroup line {
}
#extensionStart, .extension {
fill: ${options.mainBkg} !important;
fill: transparent !important;
stroke: ${options.lineColor} !important;
stroke-width: 1;
}
#extensionEnd, .extension {
fill: ${options.mainBkg} !important;
fill: transparent !important;
stroke: ${options.lineColor} !important;
stroke-width: 1;
}
#aggregationStart, .aggregation {
fill: ${options.mainBkg} !important;
fill: transparent !important;
stroke: ${options.lineColor} !important;
stroke-width: 1;
}
#aggregationEnd, .aggregation {
fill: ${options.mainBkg} !important;
fill: transparent !important;
stroke: ${options.lineColor} !important;
stroke-width: 1;
}

View File

@@ -172,7 +172,6 @@ export const drawClass = function (elem, classDef, conf, diagObj) {
// add class group
const g = elem.append('g').attr('id', diagObj.db.lookUpDomId(id)).attr('class', 'classGroup');
// add title
let title;
if (classDef.link) {
title = g
@@ -209,47 +208,56 @@ export const drawClass = function (elem, classDef, conf, diagObj) {
}
const titleHeight = title.node().getBBox().height;
let membersLine;
let membersBox;
let methodsLine;
const membersLine = g
.append('line') // text label for the x axis
.attr('x1', 0)
.attr('y1', conf.padding + titleHeight + conf.dividerMargin / 2)
.attr('y2', conf.padding + titleHeight + conf.dividerMargin / 2);
// don't draw box if no members
if (classDef.members.length > 0) {
membersLine = g
.append('line') // text label for the x axis
.attr('x1', 0)
.attr('y1', conf.padding + titleHeight + conf.dividerMargin / 2)
.attr('y2', conf.padding + titleHeight + conf.dividerMargin / 2);
const members = g
.append('text') // text label for the x axis
.attr('x', conf.padding)
.attr('y', titleHeight + conf.dividerMargin + conf.textHeight)
.attr('fill', 'white')
.attr('class', 'classText');
const members = g
.append('text') // text label for the x axis
.attr('x', conf.padding)
.attr('y', titleHeight + conf.dividerMargin + conf.textHeight)
.attr('fill', 'white')
.attr('class', 'classText');
isFirst = true;
classDef.members.forEach(function (member) {
addTspan(members, member, isFirst, conf);
isFirst = false;
});
isFirst = true;
classDef.members.forEach(function (member) {
addTspan(members, member, isFirst, conf);
isFirst = false;
});
const membersBox = members.node().getBBox();
membersBox = members.node().getBBox();
}
const methodsLine = g
.append('line') // text label for the x axis
.attr('x1', 0)
.attr('y1', conf.padding + titleHeight + conf.dividerMargin + membersBox.height)
.attr('y2', conf.padding + titleHeight + conf.dividerMargin + membersBox.height);
// don't draw box if no methods
if (classDef.methods.length > 0) {
methodsLine = g
.append('line') // text label for the x axis
.attr('x1', 0)
.attr('y1', conf.padding + titleHeight + conf.dividerMargin + membersBox.height)
.attr('y2', conf.padding + titleHeight + conf.dividerMargin + membersBox.height);
const methods = g
.append('text') // text label for the x axis
.attr('x', conf.padding)
.attr('y', titleHeight + 2 * conf.dividerMargin + membersBox.height + conf.textHeight)
.attr('fill', 'white')
.attr('class', 'classText');
const methods = g
.append('text') // text label for the x axis
.attr('x', conf.padding)
.attr('y', titleHeight + 2 * conf.dividerMargin + membersBox.height + conf.textHeight)
.attr('fill', 'white')
.attr('class', 'classText');
isFirst = true;
isFirst = true;
classDef.methods.forEach(function (method) {
addTspan(methods, method, isFirst, conf);
isFirst = false;
});
classDef.methods.forEach(function (method) {
addTspan(methods, method, isFirst, conf);
isFirst = false;
});
}
const classBox = g.node().getBBox();
var cssClassStr = ' ';
@@ -278,8 +286,12 @@ export const drawClass = function (elem, classDef, conf, diagObj) {
title.insert('title').text(classDef.tooltip);
}
membersLine.attr('x2', rectWidth);
methodsLine.attr('x2', rectWidth);
if (membersLine) {
membersLine.attr('x2', rectWidth);
}
if (methodsLine) {
methodsLine.attr('x2', rectWidth);
}
classInfo.width = rectWidth;
classInfo.height = classBox.height + conf.padding + 0.5 * conf.dividerMargin;
@@ -291,7 +303,7 @@ export const getClassTitleString = function (classDef) {
let classTitleString = classDef.id;
if (classDef.type) {
classTitleString += '<' + classDef.type + '>';
classTitleString += '<' + parseGenericTypes(classDef.type) + '>';
}
return classTitleString;
@@ -360,82 +372,19 @@ export const drawNote = function (elem, note, conf, diagObj) {
return noteInfo;
};
export const parseMember = function (text) {
let displayText = '';
let cssStyle = '';
let returnType = '';
let visibility = '';
let firstChar = text.substring(0, 1);
let lastChar = text.substring(text.length - 1, text.length);
if (firstChar.match(/[#+~-]/)) {
visibility = firstChar;
}
let noClassifierRe = /[\s\w)~]/;
if (!lastChar.match(noClassifierRe)) {
cssStyle = parseClassifier(lastChar);
}
const startIndex = visibility === '' ? 0 : 1;
let endIndex = cssStyle === '' ? text.length : text.length - 1;
text = text.substring(startIndex, endIndex);
const methodStart = text.indexOf('(');
const methodEnd = text.indexOf(')');
const isMethod = methodStart > 1 && methodEnd > methodStart && methodEnd <= text.length;
if (isMethod) {
let methodName = text.substring(0, methodStart).trim();
const parameters = text.substring(methodStart + 1, methodEnd);
displayText = visibility + methodName + '(' + parseGenericTypes(parameters.trim()) + ')';
if (methodEnd < text.length) {
// special case: classifier after the closing parenthesis
let potentialClassifier = text.substring(methodEnd + 1, methodEnd + 2);
if (cssStyle === '' && !potentialClassifier.match(noClassifierRe)) {
cssStyle = parseClassifier(potentialClassifier);
returnType = text.substring(methodEnd + 2).trim();
} else {
returnType = text.substring(methodEnd + 1).trim();
}
if (returnType !== '') {
if (returnType.charAt(0) === ':') {
returnType = returnType.substring(1).trim();
}
returnType = ' : ' + parseGenericTypes(returnType);
displayText += returnType;
}
}
} else {
// finally - if all else fails, just send the text back as written (other than parsing for generic types)
displayText = visibility + parseGenericTypes(text);
}
return {
displayText,
cssStyle,
};
};
/**
* Adds a <tspan> for a member in a diagram
*
* @param {SVGElement} textEl The element to append to
* @param {string} txt The member
* @param {string} member The member
* @param {boolean} isFirst
* @param {{ padding: string; textHeight: string }} conf The configuration for the member
*/
const addTspan = function (textEl, txt, isFirst, conf) {
let member = parseMember(txt);
const addTspan = function (textEl, member, isFirst, conf) {
const { displayText, cssStyle } = member.getDisplayDetails();
const tSpan = textEl.append('tspan').attr('x', conf.padding).text(displayText);
const tSpan = textEl.append('tspan').attr('x', conf.padding).text(member.displayText);
if (member.cssStyle !== '') {
if (cssStyle !== '') {
tSpan.attr('style', member.cssStyle);
}
@@ -444,27 +393,9 @@ const addTspan = function (textEl, txt, isFirst, conf) {
}
};
/**
* Gives the styles for a classifier
*
* @param {'+' | '-' | '#' | '~' | '*' | '$'} classifier The classifier string
* @returns {string} Styling for the classifier
*/
const parseClassifier = function (classifier) {
switch (classifier) {
case '*':
return 'font-style:italic;';
case '$':
return 'text-decoration:underline;';
default:
return '';
}
};
export default {
getClassTitleString,
drawClass,
drawEdge,
drawNote,
parseMember,
};

View File

@@ -1,330 +1,27 @@
import svgDraw from './svgDraw.js';
import { JSDOM } from 'jsdom';
describe('given a string representing class method, ', function () {
it('should handle class names with generics', function () {
const classDef = {
id: 'Car',
type: 'T',
label: 'Car',
};
describe('given a string representing a class, ', function () {
describe('when class name includes generic, ', function () {
it('should return correct text for generic', function () {
const classDef = {
id: 'Car',
type: 'T',
label: 'Car',
};
let actual = svgDraw.getClassTitleString(classDef);
expect(actual).toBe('Car<T>');
});
describe('when parsing base method declaration', function () {
it('should handle simple declaration', function () {
const str = 'foo()';
let actual = svgDraw.parseMember(str);
expect(actual.displayText).toBe('foo()');
expect(actual.cssStyle).toBe('');
let actual = svgDraw.getClassTitleString(classDef);
expect(actual).toBe('Car<T>');
});
it('should return correct text for nested generics', function () {
const classDef = {
id: 'Car',
type: 'T~T~',
label: 'Car',
};
it('should handle declaration with parameters', function () {
const str = 'foo(int id)';
let actual = svgDraw.parseMember(str);
expect(actual.displayText).toBe('foo(int id)');
expect(actual.cssStyle).toBe('');
});
it('should handle declaration with multiple parameters', function () {
const str = 'foo(int id, object thing)';
let actual = svgDraw.parseMember(str);
expect(actual.displayText).toBe('foo(int id, object thing)');
expect(actual.cssStyle).toBe('');
});
it('should handle declaration with single item in parameters', function () {
const str = 'foo(id)';
let actual = svgDraw.parseMember(str);
expect(actual.displayText).toBe('foo(id)');
expect(actual.cssStyle).toBe('');
});
it('should handle declaration with single item in parameters with extra spaces', function () {
const str = ' foo ( id) ';
let actual = svgDraw.parseMember(str);
expect(actual.displayText).toBe('foo(id)');
expect(actual.cssStyle).toBe('');
});
it('should handle method declaration with generic parameter', function () {
const str = 'foo(List~int~)';
let actual = svgDraw.parseMember(str);
expect(actual.displayText).toBe('foo(List<int>)');
expect(actual.cssStyle).toBe('');
});
it('should handle method declaration with normal and generic parameter', function () {
const str = 'foo(int, List~int~)';
let actual = svgDraw.parseMember(str);
expect(actual.displayText).toBe('foo(int, List<int>)');
expect(actual.cssStyle).toBe('');
});
it('should handle declaration with return value', function () {
const str = 'foo(id) int';
let actual = svgDraw.parseMember(str);
expect(actual.displayText).toBe('foo(id) : int');
expect(actual.cssStyle).toBe('');
});
it('should handle declaration with colon return value', function () {
const str = 'foo(id) : int';
let actual = svgDraw.parseMember(str);
expect(actual.displayText).toBe('foo(id) : int');
expect(actual.cssStyle).toBe('');
});
it('should handle declaration with generic return value', function () {
const str = 'foo(id) List~int~';
let actual = svgDraw.parseMember(str);
expect(actual.displayText).toBe('foo(id) : List<int>');
expect(actual.cssStyle).toBe('');
});
it('should handle declaration with colon generic return value', function () {
const str = 'foo(id) : List~int~';
let actual = svgDraw.parseMember(str);
expect(actual.displayText).toBe('foo(id) : List<int>');
expect(actual.cssStyle).toBe('');
});
it('should handle method declaration with all possible markup', function () {
const str = '+foo ( List~int~ ids )* List~Item~';
let actual = svgDraw.parseMember(str);
expect(actual.displayText).toBe('+foo(List<int> ids) : List<Item>');
expect(actual.cssStyle).toBe('font-style:italic;');
});
it('should handle method declaration with nested generics', function () {
const str = '+foo ( List~List~int~~ ids )* List~List~Item~~';
let actual = svgDraw.parseMember(str);
expect(actual.displayText).toBe('+foo(List<List<int>> ids) : List<List<Item>>');
expect(actual.cssStyle).toBe('font-style:italic;');
});
});
describe('when parsing method visibility', function () {
it('should correctly handle public', function () {
const str = '+foo()';
let actual = svgDraw.parseMember(str);
expect(actual.displayText).toBe('+foo()');
expect(actual.cssStyle).toBe('');
});
it('should correctly handle private', function () {
const str = '-foo()';
let actual = svgDraw.parseMember(str);
expect(actual.displayText).toBe('-foo()');
expect(actual.cssStyle).toBe('');
});
it('should correctly handle protected', function () {
const str = '#foo()';
let actual = svgDraw.parseMember(str);
expect(actual.displayText).toBe('#foo()');
expect(actual.cssStyle).toBe('');
});
it('should correctly handle package/internal', function () {
const str = '~foo()';
let actual = svgDraw.parseMember(str);
expect(actual.displayText).toBe('~foo()');
expect(actual.cssStyle).toBe('');
});
});
describe('when parsing method classifier', function () {
it('should handle abstract method', function () {
const str = 'foo()*';
let actual = svgDraw.parseMember(str);
expect(actual.displayText).toBe('foo()');
expect(actual.cssStyle).toBe('font-style:italic;');
});
it('should handle abstract method with return type', function () {
const str = 'foo(name: String) int*';
let actual = svgDraw.parseMember(str);
expect(actual.displayText).toBe('foo(name: String) : int');
expect(actual.cssStyle).toBe('font-style:italic;');
});
it('should handle abstract method classifier after parenthesis with return type', function () {
const str = 'foo(name: String)* int';
let actual = svgDraw.parseMember(str);
expect(actual.displayText).toBe('foo(name: String) : int');
expect(actual.cssStyle).toBe('font-style:italic;');
});
it('should handle static method classifier', function () {
const str = 'foo()$';
let actual = svgDraw.parseMember(str);
expect(actual.displayText).toBe('foo()');
expect(actual.cssStyle).toBe('text-decoration:underline;');
});
it('should handle static method classifier with return type', function () {
const str = 'foo(name: String) int$';
let actual = svgDraw.parseMember(str);
expect(actual.displayText).toBe('foo(name: String) : int');
expect(actual.cssStyle).toBe('text-decoration:underline;');
});
it('should handle static method classifier with colon and return type', function () {
const str = 'foo(name: String): int$';
let actual = svgDraw.parseMember(str);
expect(actual.displayText).toBe('foo(name: String) : int');
expect(actual.cssStyle).toBe('text-decoration:underline;');
});
it('should handle static method classifier after parenthesis with return type', function () {
const str = 'foo(name: String)$ int';
let actual = svgDraw.parseMember(str);
expect(actual.displayText).toBe('foo(name: String) : int');
expect(actual.cssStyle).toBe('text-decoration:underline;');
});
it('should ignore unknown character for classifier', function () {
const str = 'foo()!';
let actual = svgDraw.parseMember(str);
expect(actual.displayText).toBe('foo()');
expect(actual.cssStyle).toBe('');
});
});
});
describe('given a string representing class member, ', function () {
describe('when parsing member declaration', function () {
it('should handle simple field', function () {
const str = 'id';
let actual = svgDraw.parseMember(str);
expect(actual.displayText).toBe('id');
expect(actual.cssStyle).toBe('');
});
it('should handle field with type', function () {
const str = 'int id';
let actual = svgDraw.parseMember(str);
expect(actual.displayText).toBe('int id');
expect(actual.cssStyle).toBe('');
});
it('should handle field with type (name first)', function () {
const str = 'id: int';
let actual = svgDraw.parseMember(str);
expect(actual.displayText).toBe('id: int');
expect(actual.cssStyle).toBe('');
});
it('should handle array field', function () {
const str = 'int[] ids';
let actual = svgDraw.parseMember(str);
expect(actual.displayText).toBe('int[] ids');
expect(actual.cssStyle).toBe('');
});
it('should handle array field (name first)', function () {
const str = 'ids: int[]';
let actual = svgDraw.parseMember(str);
expect(actual.displayText).toBe('ids: int[]');
expect(actual.cssStyle).toBe('');
});
it('should handle field with generic type', function () {
const str = 'List~int~ ids';
let actual = svgDraw.parseMember(str);
expect(actual.displayText).toBe('List<int> ids');
expect(actual.cssStyle).toBe('');
});
it('should handle field with generic type (name first)', function () {
const str = 'ids: List~int~';
let actual = svgDraw.parseMember(str);
expect(actual.displayText).toBe('ids: List<int>');
expect(actual.cssStyle).toBe('');
});
});
describe('when parsing classifiers', function () {
it('should handle static field', function () {
const str = 'String foo$';
let actual = svgDraw.parseMember(str);
expect(actual.displayText).toBe('String foo');
expect(actual.cssStyle).toBe('text-decoration:underline;');
});
it('should handle static field (name first)', function () {
const str = 'foo: String$';
let actual = svgDraw.parseMember(str);
expect(actual.displayText).toBe('foo: String');
expect(actual.cssStyle).toBe('text-decoration:underline;');
});
it('should handle static field with generic type', function () {
const str = 'List~String~ foo$';
let actual = svgDraw.parseMember(str);
expect(actual.displayText).toBe('List<String> foo');
expect(actual.cssStyle).toBe('text-decoration:underline;');
});
it('should handle static field with generic type (name first)', function () {
const str = 'foo: List~String~$';
let actual = svgDraw.parseMember(str);
expect(actual.displayText).toBe('foo: List<String>');
expect(actual.cssStyle).toBe('text-decoration:underline;');
});
it('should handle field with nested generic type', function () {
const str = 'List~List~int~~ idLists';
let actual = svgDraw.parseMember(str);
expect(actual.displayText).toBe('List<List<int>> idLists');
expect(actual.cssStyle).toBe('');
});
it('should handle field with nested generic type (name first)', function () {
const str = 'idLists: List~List~int~~';
let actual = svgDraw.parseMember(str);
expect(actual.displayText).toBe('idLists: List<List<int>>');
expect(actual.cssStyle).toBe('');
let actual = svgDraw.getClassTitleString(classDef);
expect(actual).toBe('Car<T<T>>');
});
});
});

View File

@@ -1,4 +1,4 @@
import { sanitizeText, removeScript, parseGenericTypes } from './common.js';
import { sanitizeText, removeScript, parseGenericTypes, countOccurrence } from './common.js';
describe('when securityLevel is antiscript, all script must be removed', () => {
/**
@@ -59,16 +59,29 @@ describe('Sanitize text', () => {
});
describe('generic parser', () => {
it('should parse generic types', () => {
expect(parseGenericTypes('test~T~')).toEqual('test<T>');
expect(parseGenericTypes('test~Array~Array~string~~~')).toEqual('test<Array<Array<string>>>');
expect(parseGenericTypes('test~Array~Array~string[]~~~')).toEqual(
'test<Array<Array<string[]>>>'
);
expect(parseGenericTypes('test ~Array~Array~string[]~~~')).toEqual(
'test <Array<Array<string[]>>>'
);
expect(parseGenericTypes('~test')).toEqual('~test');
expect(parseGenericTypes('~test Array~string~')).toEqual('~test Array<string>');
it.each([
['test~T~', 'test<T>'],
['test~Array~Array~string~~~', 'test<Array<Array<string>>>'],
['test~Array~Array~string[]~~~', 'test<Array<Array<string[]>>>'],
['test ~Array~Array~string[]~~~', 'test <Array<Array<string[]>>>'],
['~test', '~test'],
['~test~T~', '~test<T>'],
])('should parse generic types: %s to %s', (input: string, expected: string) => {
expect(parseGenericTypes(input)).toEqual(expected);
});
});
it.each([
['', '', 0],
['', 'x', 0],
['test', 'x', 0],
['test', 't', 2],
['test', 'te', 1],
['test~T~', '~', 2],
['test~Array~Array~string~~~', '~', 6],
])(
'should count `%s` to contain occurrences of `%s` to be `%i`',
(str: string, substring: string, count: number) => {
expect(countOccurrence(str, substring)).toEqual(count);
}
);

View File

@@ -1,5 +1,5 @@
import DOMPurify from 'dompurify';
import { MermaidConfig } from '../../config.type.js';
import type { MermaidConfig } from '../../config.type.js';
// Remove and ignore br:s
export const lineBreakRegex = /<br\s*\/?>/gi;
@@ -178,23 +178,80 @@ export const getMin = function (...values: number[]): number {
* @param text - The text to convert
* @returns The converted string
*/
export const parseGenericTypes = function (text: string): string {
let cleanedText = text;
export const parseGenericTypes = function (input: string): string {
const inputSets = input.split(/(,)/);
const output = [];
if (text.split('~').length - 1 >= 2) {
let newCleanedText = cleanedText;
for (let i = 0; i < inputSets.length; i++) {
let thisSet = inputSets[i];
// use a do...while loop instead of replaceAll to detect recursion
// e.g. Array~Array~T~~
do {
cleanedText = newCleanedText;
newCleanedText = cleanedText.replace(/~([^\s,:;]+)~/, '<$1>');
} while (newCleanedText != cleanedText);
// if the original input included a value such as "~K, V~"", these will be split into
// an array of ["~K",","," V~"].
// This means that on each call of processSet, there will only be 1 ~ present
// To account for this, if we encounter a ",", we are checking the previous and next sets in the array
// to see if they contain matching ~'s
// in which case we are assuming that they should be rejoined and sent to be processed
if (thisSet === ',' && i > 0 && i + 1 < inputSets.length) {
const previousSet = inputSets[i - 1];
const nextSet = inputSets[i + 1];
return parseGenericTypes(newCleanedText);
} else {
return cleanedText;
if (shouldCombineSets(previousSet, nextSet)) {
thisSet = previousSet + ',' + nextSet;
i++; // Move the index forward to skip the next iteration since we're combining sets
output.pop();
}
}
output.push(processSet(thisSet));
}
return output.join('');
};
export const countOccurrence = (string: string, substring: string): number => {
return Math.max(0, string.split(substring).length - 1);
};
const shouldCombineSets = (previousSet: string, nextSet: string): boolean => {
const prevCount = countOccurrence(previousSet, '~');
const nextCount = countOccurrence(nextSet, '~');
return prevCount === 1 && nextCount === 1;
};
const processSet = (input: string): string => {
const tildeCount = countOccurrence(input, '~');
let hasStartingTilde = false;
if (tildeCount <= 1) {
return input;
}
// If there is an odd number of tildes, and the input starts with a tilde, we need to remove it and add it back in later
if (tildeCount % 2 !== 0 && input.startsWith('~')) {
input = input.substring(1);
hasStartingTilde = true;
}
const chars = [...input];
let first = chars.indexOf('~');
let last = chars.lastIndexOf('~');
while (first !== -1 && last !== -1 && first !== last) {
chars[first] = '<';
chars[last] = '>';
first = chars.indexOf('~');
last = chars.lastIndexOf('~');
}
// Add the starting tilde back in if we removed it
if (hasStartingTilde) {
chars.unshift('~');
}
return chars.join('');
};
export default {

View File

@@ -0,0 +1,32 @@
import { sanitizeText as _sanitizeText } from './common.js';
import { getConfig } from '../../config.js';
let accTitle = '';
let diagramTitle = '';
let accDescription = '';
const sanitizeText = (txt: string): string => _sanitizeText(txt, getConfig());
export const clear = (): void => {
accTitle = '';
accDescription = '';
diagramTitle = '';
};
export const setAccTitle = (txt: string): void => {
accTitle = sanitizeText(txt).replace(/^\s+/g, '');
};
export const getAccTitle = (): string => accTitle;
export const setAccDescription = (txt: string): void => {
accDescription = sanitizeText(txt).replace(/\n\s+/g, '\n');
};
export const getAccDescription = (): string => accDescription;
export const setDiagramTitle = (txt: string): void => {
diagramTitle = sanitizeText(txt);
};
export const getDiagramTitle = (): string => diagramTitle;

View File

@@ -1,5 +1,4 @@
import { log } from '../../logger.js';
import mermaidAPI from '../../mermaidAPI.js';
import * as configApi from '../../config.js';
import {
@@ -10,7 +9,7 @@ import {
clear as commonClear,
setDiagramTitle,
getDiagramTitle,
} from '../../commonDb.js';
} from '../common/commonDb.js';
let entities = {};
let relationships = [];
@@ -28,14 +27,13 @@ const Identification = {
IDENTIFYING: 'IDENTIFYING',
};
export const parseDirective = function (statement, context, type) {
mermaidAPI.parseDirective(this, statement, context, type);
};
const addEntity = function (name) {
const addEntity = function (name, alias = undefined) {
if (entities[name] === undefined) {
entities[name] = { attributes: [] };
entities[name] = { attributes: [], alias: alias };
log.info('Added new entity :', name);
} else if (entities[name] && !entities[name].alias && alias) {
entities[name].alias = alias;
log.info(`Add alias '${alias}' to entity '${name}'`);
}
return entities[name];
@@ -85,7 +83,6 @@ const clear = function () {
export default {
Cardinality,
Identification,
parseDirective,
getConfig: () => configApi.getConfig().er,
addEntity,
addAttributes,

View File

@@ -326,7 +326,7 @@ const drawEntities = function (svgNode, entities, graph) {
.style('text-anchor', 'middle')
.style('font-family', getConfig().fontFamily)
.style('font-size', conf.fontSize + 'px')
.text(entityName);
.text(entities[entityName].alias ?? entityName);
const { width: entityWidth, height: entityHeight } = drawAttributes(
groupNode,

View File

@@ -1,7 +1,7 @@
%lex
%options case-insensitive
%x open_directive type_directive arg_directive block
%x block
%x acc_title
%x acc_descr
%x acc_descr_multiline
@@ -14,11 +14,6 @@ accDescr\s*":"\s* { this.begin("ac
accDescr\s*"{"\s* { this.begin("acc_descr_multiline");}
<acc_descr_multiline>[\}] { this.popState(); }
<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';
\s+ /* skip whitespace */
[\s]+ return 'SPACE';
@@ -35,6 +30,8 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multili
<block>[\n]+ /* nothing */
<block>"}" { this.popState(); return 'BLOCK_STOP'; }
<block>. return yytext[0];
"[" return 'SQS';
"]" return 'SQE';
"one or zero" return 'ZERO_OR_ONE';
"one or more" return 'ONE_OR_MORE';
@@ -64,7 +61,7 @@ o\{ return 'ZERO_OR_MORE';
"optionally to" return 'NON_IDENTIFYING';
\.\- return 'NON_IDENTIFYING';
\-\. return 'NON_IDENTIFYING';
[A-Za-z][A-Za-z0-9\-_]* return 'ALPHANUM';
[A-Za-z_][A-Za-z0-9\-_]* return 'ALPHANUM';
. return yytext[0];
<<EOF>> return 'EOF';
@@ -75,7 +72,6 @@ o\{ return 'ZERO_OR_MORE';
start
: 'ER_DIAGRAM' document 'EOF' { /*console.log('finished parsing');*/ }
| directive start
;
document
@@ -90,29 +86,28 @@ line
| EOF { $$=[];}
;
directive
: openDirective typeDirective closeDirective 'NEWLINE'
| openDirective typeDirective ':' argDirective closeDirective 'NEWLINE'
;
statement
: directive
| entityName relSpec entityName ':' role
: entityName relSpec entityName ':' role
{
yy.addEntity($1);
yy.addEntity($3);
yy.addRelationship($1, $5, $3, $2);
/*console.log($1 + $2 + $3 + ':' + $5);*/
}
| entityName BLOCK_START attributes BLOCK_STOP
{
/* console.log('detected block'); */
yy.addEntity($1);
yy.addAttributes($1, $3);
/* console.log('handled block'); */
}
| entityName BLOCK_START BLOCK_STOP { yy.addEntity($1); }
| entityName { yy.addEntity($1); }
| entityName SQS entityName SQE BLOCK_START attributes BLOCK_STOP
{
yy.addEntity($1, $3);
yy.addAttributes($1, $6);
}
| entityName SQS entityName SQE BLOCK_START BLOCK_STOP { yy.addEntity($1, $3); }
| entityName SQS entityName SQE { yy.addEntity($1, $3); }
| title title_value { $$=$2.trim();yy.setAccTitle($$); }
| acc_title acc_title_value { $$=$2.trim();yy.setAccTitle($$); }
| acc_descr acc_descr_value { $$=$2.trim();yy.setAccDescription($$); }
@@ -185,20 +180,4 @@ role
| '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

@@ -33,7 +33,7 @@ describe('when parsing ER diagram it...', function () {
describe('has non A-Za-z0-9_- chars', function () {
// these were entered using the Mac keyboard utility.
const chars =
"~ ` ! @ # $ ^ & * ( ) - _ = + [ ] { } | / ; : ' . ? ¡ ™ € £ ¢ ∞ fi § ‡ • ° ª · º ≠ ± œ Œ ∑ „ ® † ˇ ¥ Á ¨ ˆ ˆ Ø π ∏ “ « » å Å ß Í ∂ Î ƒ Ï © ˙ Ó ∆ Ô ˚  ¬ Ò … Ú æ Æ Ω ¸ ≈ π ˛ ç Ç √ ◊ ∫ ı ˜ µ  ≤ ¯ ≥ ˘ ÷ ¿";
"~ ` ! @ # $ ^ & * ( ) - = + [ ] { } | / ; : ' . ? ¡ ™ € £ ¢ ∞ fi § ‡ • ° ª · º ≠ ± œ Œ ∑ „ ® † ˇ ¥ Á ¨ ˆ ˆ Ø π ∏ “ « » å Å ß Í ∂ Î ƒ Ï © ˙ Ó ∆ Ô ˚  ¬ Ò … Ú æ Æ Ω ¸ ≈ π ˛ ç Ç √ ◊ ∫ ı ˜ µ  ≤ ¯ ≥ ˘ ÷ ¿";
const allowed = chars.split(' ');
allowed.forEach((allowedChar) => {
@@ -133,6 +133,50 @@ describe('when parsing ER diagram it...', function () {
const entities = erDb.getEntities();
expect(entities.hasOwnProperty(hyphensUnderscore)).toBe(true);
});
it('can have an alias', function () {
const entity = 'foo';
const alias = 'bar';
erDiagram.parser.parse(`erDiagram\n${entity}["${alias}"]\n`);
const entities = erDb.getEntities();
expect(entities.hasOwnProperty(entity)).toBe(true);
expect(entities[entity].alias).toBe(alias);
});
it('can have an alias even if the relationship is defined before class', function () {
const firstEntity = 'foo';
const secondEntity = 'bar';
const alias = 'batman';
erDiagram.parser.parse(
`erDiagram\n${firstEntity} ||--o| ${secondEntity} : rel\nclass ${firstEntity}["${alias}"]\n`
);
const entities = erDb.getEntities();
expect(entities.hasOwnProperty(firstEntity)).toBe(true);
expect(entities.hasOwnProperty(secondEntity)).toBe(true);
expect(entities[firstEntity].alias).toBe(alias);
expect(entities[secondEntity].alias).toBeUndefined();
});
it('can have an alias even if the relationship is defined after class', function () {
const firstEntity = 'foo';
const secondEntity = 'bar';
const alias = 'batman';
erDiagram.parser.parse(
`erDiagram\nclass ${firstEntity}["${alias}"]\n${firstEntity} ||--o| ${secondEntity} : rel\n`
);
const entities = erDb.getEntities();
expect(entities.hasOwnProperty(firstEntity)).toBe(true);
expect(entities.hasOwnProperty(secondEntity)).toBe(true);
expect(entities[firstEntity].alias).toBe(alias);
expect(entities[secondEntity].alias).toBeUndefined();
});
it('can start with an underscore', function () {
const entity = '_foo';
erDiagram.parser.parse(`erDiagram\n${entity}\n`);
const entities = erDb.getEntities();
expect(entities.hasOwnProperty(entity)).toBe(true);
});
});
describe('attribute name', () => {

View File

@@ -1,4 +1,5 @@
import { findCommonAncestor, TreeData } from './render-utils.js';
import type { TreeData } from './render-utils.js';
import { findCommonAncestor } from './render-utils.js';
describe('when rendering a flowchart using elk ', () => {
let lookupDb: TreeData;
beforeEach(() => {

View File

@@ -2,7 +2,6 @@ import { select } from 'd3';
import utils from '../../utils.js';
import * as configApi from '../../config.js';
import common from '../common/common.js';
import mermaidAPI from '../../mermaidAPI.js';
import { log } from '../../logger.js';
import {
setAccTitle,
@@ -12,7 +11,7 @@ import {
clear as commonClear,
setDiagramTitle,
getDiagramTitle,
} from '../../commonDb.js';
} from '../common/commonDb.js';
const MERMAID_DOM_ID_PREFIX = 'flowchart-';
let vertexCounter = 0;
@@ -34,10 +33,6 @@ let funs = [];
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.
*
@@ -771,7 +766,6 @@ export const lex = {
firstGraph,
};
export default {
parseDirective,
defaultConfig: () => configApi.defaultConfig.flowchart,
setAccTitle,
getAccTitle,

View File

@@ -3,7 +3,7 @@ import flowParser from './parser/flow.jison';
import flowDb from './flowDb.js';
import flowRendererV2 from './flowRenderer-v2.js';
import flowStyles from './styles.js';
import { MermaidConfig } from '../../config.type.js';
import type { MermaidConfig } from '../../config.type.js';
import { setConfig } from '../../config.js';
export const diagram = {

View File

@@ -4,7 +4,7 @@ import flowDb from './flowDb.js';
import flowRenderer from './flowRenderer.js';
import flowRendererV2 from './flowRenderer-v2.js';
import flowStyles from './styles.js';
import { MermaidConfig } from '../../config.type.js';
import type { MermaidConfig } from '../../config.type.js';
export const diagram = {
parser: flowParser,

View File

@@ -23,17 +23,8 @@
%x href
%x callbackname
%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'; }
<acc_title>(?!\n|;|#)*[^\n]* { this.popState(); return "acc_title_value"; }
accDescr\s*":"\s* { this.begin("acc_descr");return 'acc_descr'; }
@@ -272,35 +263,10 @@ that id.
%% /* language grammar */
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
;
document
: /* empty */
{ $$ = [];}

View File

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

View File

@@ -3,7 +3,7 @@ import ganttParser from './parser/gantt.jison';
import ganttDb from './ganttDb.js';
import ganttRenderer from './ganttRenderer.js';
import ganttStyles from './styles.js';
import { DiagramDefinition } from '../../diagram-api/types.js';
import type { DiagramDefinition } from '../../diagram-api/types.js';
export const diagram: DiagramDefinition = {
parser: ganttParser,

View File

@@ -10,6 +10,8 @@ import {
axisBottom,
axisTop,
timeFormat,
timeMillisecond,
timeSecond,
timeMinute,
timeHour,
timeDay,
@@ -573,7 +575,7 @@ export const draw = function (text, id, version, diagObj) {
.tickSize(-h + theTopPad + conf.gridLineStartPadding)
.tickFormat(timeFormat(diagObj.db.getAxisFormat() || conf.axisFormat || '%Y-%m-%d'));
const reTickInterval = /^([1-9]\d*)(minute|hour|day|week|month)$/;
const reTickInterval = /^([1-9]\d*)(millisecond|second|minute|hour|day|week|month)$/;
const resultTickInterval = reTickInterval.exec(
diagObj.db.getTickInterval() || conf.tickInterval
);
@@ -584,6 +586,12 @@ export const draw = function (text, id, version, diagObj) {
const weekday = diagObj.db.getWeekday() || conf.weekday;
switch (interval) {
case 'millisecond':
bottomXAxis.ticks(timeMillisecond.every(every));
break;
case 'second':
bottomXAxis.ticks(timeSecond.every(every));
break;
case 'minute':
bottomXAxis.ticks(timeMinute.every(every));
break;
@@ -625,6 +633,12 @@ export const draw = function (text, id, version, diagObj) {
const weekday = diagObj.db.getWeekday() || conf.weekday;
switch (interval) {
case 'millisecond':
topXAxis.ticks(timeMillisecond.every(every));
break;
case 'second':
topXAxis.ticks(timeSecond.every(every));
break;
case 'minute':
topXAxis.ticks(timeMinute.every(every));
break;

View File

@@ -11,19 +11,11 @@
%x href
%x callbackname
%x callbackargs
%x open_directive
%x type_directive
%x arg_directive
%x close_directive
%x acc_title
%x acc_descr
%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';
accTitle\s*":"\s* { this.begin("acc_title");return 'acc_title'; }
<acc_title>(?!\n|;|#)*[^\n]* { this.popState(); return "acc_title_value"; }
@@ -112,8 +104,7 @@ weekday\s+sunday return 'weekday_sunday'
%% /* language grammar */
start
: directive start
| gantt document 'EOF' { return $2; }
: gantt document 'EOF' { return $2; }
;
document
@@ -155,13 +146,8 @@ statement
| section { yy.addSection($1.substr(8));$$=$1.substr(8); }
| clickStatement
| 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.
@@ -192,20 +178,4 @@ clickStatementDebug
| 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 { random } from '../../utils.js';
import mermaidAPI from '../../mermaidAPI.js';
import * as configApi from '../../config.js';
import { getConfig } from '../../config.js';
import common from '../common/common.js';
@@ -12,7 +11,7 @@ import {
clear as commonClear,
setDiagramTitle,
getDiagramTitle,
} from '../../commonDb.js';
} from '../common/commonDb.js';
let mainBranchName = getConfig().gitGraph.mainBranchName;
let mainBranchOrder = getConfig().gitGraph.mainBranchOrder;
@@ -33,10 +32,6 @@ function getId() {
return random({ length: 7 });
}
export const parseDirective = function (statement, context, type) {
mermaidAPI.parseDirective(this, statement, context, type);
};
// /**
// * @param currentCommit
// * @param otherCommit
@@ -507,7 +502,6 @@ export const commitType = {
};
export default {
parseDirective,
getConfig: () => configApi.getConfig().gitGraph,
setDirection,
setOptions,

View File

@@ -3,7 +3,7 @@ import gitGraphParser from './parser/gitGraph.jison';
import gitGraphDb from './gitGraphAst.js';
import gitGraphRenderer from './gitGraphRenderer.js';
import gitGraphStyles from './styles.js';
import { DiagramDefinition } from '../../diagram-api/types.js';
import type { DiagramDefinition } from '../../diagram-api/types.js';
export const diagram: DiagramDefinition = {
parser: gitGraphParser,

View File

@@ -1,87 +1,72 @@
import gitGraphAst from './gitGraphAst.js';
import { parser } from './parser/gitGraph.jison';
// Todo reintroduce without cryptoRandomString
import gitGraphAst from './gitGraphAst';
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() {
describe('when parsing a gitGraph', function () {
beforeEach(function () {
parser.yy = gitGraphAst;
parser.yy.clear();
randomNumber = 0;
cryptoRandomString.mockImplementation(() => {
randomNumber = randomNumber + 1;
return String(randomNumber);
});
});
afterEach(function() {
cryptoRandomString.mockReset();
});
it('should handle a gitGraph definition', function() {
it('should handle a gitGraph definition', function () {
const str = 'gitGraph:\n' + 'commit\n';
parser.parse(str);
const commits = parser.yy.getCommits();
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(Object.keys(parser.yy.getBranches()).length).toBe(1);
});
it('should handle a gitGraph definition with empty options', function() {
const str = 'gitGraph:\n' + 'options\n' + 'end\n' + 'commit\n';
it('should handle a gitGraph definition with empty options', function () {
const str = 'gitGraph:\n' + 'options\n' + ' end\n' + 'commit\n';
parser.parse(str);
const commits = parser.yy.getCommits();
expect(parser.yy.getOptions()).toEqual({});
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(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';
parser.parse(str);
const commits = parser.yy.getCommits();
expect(parser.yy.getOptions()['key']).toBe('value');
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(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';
parser.parse(str);
const commits = parser.yy.getCommits();
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(Object.keys(parser.yy.getBranches()).length).toBe(1);
});
it('should handle set direction', function() {
const str = 'gitGraph BT:\n' + 'commit\n';
it('should handle set direction', function () {
const str = 'gitGraph TB:\n' + 'commit\n';
parser.parse(str);
const commits = parser.yy.getCommits();
expect(Object.keys(commits).length).toBe(1);
expect(parser.yy.getCurrentBranch()).toBe('master');
expect(parser.yy.getDirection()).toBe('BT');
expect(parser.yy.getCurrentBranch()).toBe('main');
expect(parser.yy.getDirection()).toBe('TB');
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';
parser.parse(str);
@@ -91,7 +76,7 @@ describe('when parsing a gitGraph', function() {
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';
parser.parse(str);
@@ -103,7 +88,7 @@ describe('when parsing a gitGraph', function() {
expect(branchCommit).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';
parser.parse(str);
@@ -112,10 +97,11 @@ describe('when parsing a gitGraph', function() {
expect(Object.keys(commits).length).toBe(1);
const key = Object.keys(commits)[0];
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 =
'gitGraph:\n' +
'commit\n' +
@@ -123,18 +109,18 @@ describe('when parsing a gitGraph', function() {
'branch newbranch\n' +
'checkout newbranch\n' +
'commit\n' +
'reset master\n';
'reset main\n';
parser.parse(str);
const commits = parser.yy.getCommits();
expect(Object.keys(commits).length).toBe(3);
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']);
});
it('reset can take an argument', function() {
it.skip('reset can take an argument', function () {
const str =
'gitGraph:\n' +
'commit\n' +
@@ -142,18 +128,18 @@ describe('when parsing a gitGraph', function() {
'branch newbranch\n' +
'checkout newbranch\n' +
'commit\n' +
'reset master^\n';
'reset main^\n';
parser.parse(str);
const commits = parser.yy.getCommits();
expect(Object.keys(commits).length).toBe(3);
expect(parser.yy.getCurrentBranch()).toBe('newbranch');
const master = commits[parser.yy.getBranches()['master']];
expect(parser.yy.getHead().id).toEqual(master.parent);
const main = commits[parser.yy.getBranches()['main']];
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 =
'gitGraph:\n' +
'commit\n' +
@@ -161,19 +147,19 @@ describe('when parsing a gitGraph', function() {
'checkout newbranch\n' +
'commit\n' +
'commit\n' +
'checkout master\n' +
'checkout main\n' +
'merge newbranch\n';
parser.parse(str);
const commits = parser.yy.getCommits();
expect(Object.keys(commits).length).toBe(3);
expect(parser.yy.getCurrentBranch()).toBe('master');
expect(parser.yy.getBranches()['newbranch']).toEqual(parser.yy.getBranches()['master']);
expect(Object.keys(commits).length).toBe(4);
expect(parser.yy.getCurrentBranch()).toBe('main');
expect(parser.yy.getBranches()['newbranch']).toEqual(parser.yy.getBranches()['main']);
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 =
'gitGraph:\n' +
'commit\n' +
@@ -181,18 +167,18 @@ describe('when parsing a gitGraph', function() {
'checkout newbranch\n' +
'commit\n' +
'commit\n' +
'merge master\n';
'merge main\n';
parser.parse(str);
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.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']);
});
it('should handle merge with 2 parents', function() {
it('should handle merge with 2 parents', function () {
const str =
'gitGraph:\n' +
'commit\n' +
@@ -200,7 +186,7 @@ describe('when parsing a gitGraph', function() {
'checkout newbranch\n' +
'commit\n' +
'commit\n' +
'checkout master\n' +
'checkout main\n' +
'commit\n' +
'merge newbranch\n';
@@ -208,12 +194,12 @@ describe('when parsing a gitGraph', function() {
const commits = parser.yy.getCommits();
expect(Object.keys(commits).length).toBe(5);
expect(parser.yy.getCurrentBranch()).toBe('master');
expect(parser.yy.getBranches()['newbranch']).not.toEqual(parser.yy.getBranches()['master']);
expect(parser.yy.getHead().id).toEqual(parser.yy.getBranches()['master']);
expect(parser.yy.getCurrentBranch()).toBe('main');
expect(parser.yy.getBranches()['newbranch']).not.toEqual(parser.yy.getBranches()['main']);
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 =
'gitGraph:\n' +
'commit\n' +
@@ -221,53 +207,25 @@ describe('when parsing a gitGraph', function() {
'checkout newbranch\n' +
'commit\n' +
'commit\n' +
'checkout master\n' +
'checkout main\n' +
'commit\n' +
'merge newbranch\n' +
'commit\n' +
'checkout newbranch\n' +
'merge master\n';
'merge main\n';
parser.parse(str);
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.getBranches()['newbranch']).toEqual(parser.yy.getBranches()['master']);
expect(parser.yy.getHead().id).toEqual(parser.yy.getBranches()['master']);
expect(parser.yy.getBranches()['newbranch']).toEqual(parser.yy.getBranches()['main']);
expect(parser.yy.getHead().id).toEqual(parser.yy.getBranches()['main']);
parser.yy.prettyPrint();
});
it('should generate a secure random ID for commits', 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() {
it('should generate an array of known branches', function () {
const str =
'gitGraph:\n' +
'commit\n' +
@@ -281,7 +239,7 @@ describe('when parsing a gitGraph', function() {
const branches = gitGraphAst.getBranchesAsObjArray();
expect(branches).toHaveLength(3);
expect(branches[0]).toHaveProperty('name', 'master');
expect(branches[0]).toHaveProperty('name', 'main');
expect(branches[1]).toHaveProperty('name', 'b1');
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 { 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 () {
let randomNumber;
beforeEach(function () {
parser.yy = gitGraphAst;
parser.yy.clear();
randomNumber = 0;
});
// afterEach(function() {
// cryptoRandomString.mockReset();
// });
it('should handle a gitGraph commit with NO pararms, get auto-genrated reandom ID', function () {
const str = `gitGraph:
commit

View File

@@ -9,10 +9,6 @@
%x string
%x options
%x open_directive
%x type_directive
%x arg_directive
%x close_directive
%x acc_title
%x acc_descr
%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'; }
<acc_title>(?!\n|;|#)*[^\n]* { this.popState(); return "acc_title_value"; }
accDescr\s*":"\s* { this.begin("acc_descr");return 'acc_descr'; }
@@ -76,7 +67,6 @@ checkout(?=\s|$) return 'CHECKOUT';
start
: eol start
| directive start
| GG document EOF{ return $3; }
| GG ':' document EOF{ return $3; }
| GG DIR ':' document EOF {yy.setDirection($2); return $4;}
@@ -240,27 +230,6 @@ commitType
| 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
: ID
| STR

View File

@@ -1,10 +0,0 @@
name,amounts
Foo, 33
Rishab, 12
Alexis, 41
Tom, 16
Courtney, 59
Christina, 38
Jack, 21
Mickey, 25
Paul, 30
1 name amounts
2 Foo 33
3 Rishab 12
4 Alexis 41
5 Tom 16
6 Courtney 59
7 Christina 38
8 Jack 21
9 Mickey 25
10 Paul 30

View File

@@ -8,19 +8,10 @@
%x string
%x title
%x open_directive
%x type_directive
%x arg_directive
%x close_directive
%x acc_title
%x acc_descr
%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 */{ /*console.log('');*/ }
[\n\r]+ return 'NEWLINE';
@@ -52,7 +43,6 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multili
start
: eol start
| directive start
| PIE document
| PIE showData document {yy.setShowData(true);}
;
@@ -73,34 +63,12 @@ statement
| acc_title acc_title_value { $$=$2.trim();yy.setAccTitle($$); }
| 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);}
| directive
;
directive
: openDirective typeDirective closeDirective
| openDirective typeDirective ':' argDirective closeDirective
;
eol
: NEWLINE
| ';'
| 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

@@ -1,132 +0,0 @@
import pieDb from '../pieDb.js';
import pie from './pie.jison';
import { setConfig } from '../../../config.js';
setConfig({
securityLevel: 'strict',
});
describe('when parsing pie', function () {
beforeEach(function () {
pie.parser.yy = pieDb;
pie.parser.yy.clear();
});
it('should handle very simple pie', function () {
const res = pie.parser.parse(`pie
"ash" : 100
`);
const sections = pieDb.getSections();
const section1 = sections['ash'];
expect(section1).toBe(100);
});
it('should handle simple pie', function () {
const res = pie.parser.parse(`pie
"ash" : 60
"bat" : 40
`);
const sections = pieDb.getSections();
const section1 = sections['ash'];
expect(section1).toBe(60);
});
it('should handle simple pie with comments', function () {
const res = pie.parser.parse(`pie
%% comments
"ash" : 60
"bat" : 40
`);
const sections = pieDb.getSections();
const section1 = sections['ash'];
expect(section1).toBe(60);
});
it('should handle simple pie with a directive', function () {
const res = pie.parser.parse(`%%{init: {'logLevel':0}}%%
pie
"ash" : 60
"bat" : 40
`);
const sections = pieDb.getSections();
const section1 = sections['ash'];
expect(section1).toBe(60);
});
it('should handle simple pie with a title', function () {
const res = pie.parser.parse(`pie title a 60/40 pie
"ash" : 60
"bat" : 40
`);
const sections = pieDb.getSections();
const title = pieDb.getDiagramTitle();
const section1 = sections['ash'];
expect(section1).toBe(60);
expect(title).toBe('a 60/40 pie');
});
it('should handle simple pie without an acc description (accDescr)', function () {
const res = pie.parser.parse(`pie title a neat chart
"ash" : 60
"bat" : 40
`);
const sections = pieDb.getSections();
const title = pieDb.getDiagramTitle();
const description = pieDb.getAccDescription();
const section1 = sections['ash'];
expect(section1).toBe(60);
expect(title).toBe('a neat chart');
expect(description).toBe('');
});
it('should handle simple pie with an acc description (accDescr)', function () {
const res = pie.parser.parse(`pie title a neat chart
accDescr: a neat description
"ash" : 60
"bat" : 40
`);
const sections = pieDb.getSections();
const title = pieDb.getDiagramTitle();
const description = pieDb.getAccDescription();
const section1 = sections['ash'];
expect(section1).toBe(60);
expect(title).toBe('a neat chart');
expect(description).toBe('a neat description');
});
it('should handle simple pie with a multiline acc description (accDescr)', function () {
const res = pie.parser.parse(`pie title a neat chart
accDescr {
a neat description
on multiple lines
}
"ash" : 60
"bat" : 40
`);
const sections = pieDb.getSections();
const title = pieDb.getDiagramTitle();
const description = pieDb.getAccDescription();
const section1 = sections['ash'];
expect(section1).toBe(60);
expect(title).toBe('a neat chart');
expect(description).toBe('a neat description\non multiple lines');
});
it('should handle simple pie with positive decimal', function () {
const res = pie.parser.parse(`pie
"ash" : 60.67
"bat" : 40
`);
const sections = pieDb.getSections();
const section1 = sections['ash'];
expect(section1).toBe(60.67);
});
it('should handle simple pie with negative decimal', function () {
expect(() => {
pie.parser.parse(`pie
"ash" : 60.67
"bat" : 40..12
`);
}).toThrowError();
});
});

View File

@@ -0,0 +1,169 @@
// @ts-ignore: JISON doesn't support types
import { parser } from './parser/pie.jison';
import { DEFAULT_PIE_DB, db } from './pieDb.js';
import { setConfig } from '../../config.js';
setConfig({
securityLevel: 'strict',
});
describe('pie', () => {
beforeAll(() => {
parser.yy = db;
});
beforeEach(() => {
parser.yy.clear();
});
describe('parse', () => {
it('should handle very simple pie', () => {
parser.parse(`pie
"ash": 100
`);
const sections = db.getSections();
expect(sections['ash']).toBe(100);
});
it('should handle simple pie', () => {
parser.parse(`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 showData', () => {
parser.parse(`pie showData
"ash" : 60
"bat" : 40
`);
expect(db.getShowData()).toBeTruthy();
const sections = db.getSections();
expect(sections['ash']).toBe(60);
expect(sections['bat']).toBe(40);
});
it('should handle simple pie with comments', () => {
parser.parse(`pie
%% comments
"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', () => {
parser.parse(`pie title a 60/40 pie
"ash" : 60
"bat" : 40
`);
expect(db.getDiagramTitle()).toBe('a 60/40 pie');
const sections = db.getSections();
expect(sections['ash']).toBe(60);
expect(sections['bat']).toBe(40);
});
it('should handle simple pie with an acc title (accTitle)', () => {
parser.parse(`pie title a neat chart
accTitle: a neat acc title
"ash" : 60
"bat" : 40
`);
expect(db.getDiagramTitle()).toBe('a neat chart');
expect(db.getAccTitle()).toBe('a neat acc title');
const sections = db.getSections();
expect(sections['ash']).toBe(60);
expect(sections['bat']).toBe(40);
});
it('should handle simple pie with an acc description (accDescr)', () => {
parser.parse(`pie title a neat chart
accDescr: a neat description
"ash" : 60
"bat" : 40
`);
expect(db.getDiagramTitle()).toBe('a neat chart');
expect(db.getAccDescription()).toBe('a neat description');
const sections = db.getSections();
expect(sections['ash']).toBe(60);
expect(sections['bat']).toBe(40);
});
it('should handle simple pie with a multiline acc description (accDescr)', () => {
parser.parse(`pie title a neat chart
accDescr {
a neat description
on multiple lines
}
"ash" : 60
"bat" : 40
`);
expect(db.getDiagramTitle()).toBe('a neat chart');
expect(db.getAccDescription()).toBe('a neat description\non multiple lines');
const sections = db.getSections();
expect(sections['ash']).toBe(60);
expect(sections['bat']).toBe(40);
});
it('should handle simple pie with positive decimal', () => {
parser.parse(`pie
"ash" : 60.67
"bat" : 40
`);
const sections = db.getSections();
expect(sections['ash']).toBe(60.67);
expect(sections['bat']).toBe(40);
});
it('should handle simple pie with negative decimal', () => {
expect(() => {
parser.parse(`pie
"ash" : -60.67
"bat" : 40.12
`);
}).toThrowError();
});
});
describe('config', () => {
it.todo('setConfig', () => {
// db.setConfig({ useWidth: 850, useMaxWidth: undefined });
const config = db.getConfig();
expect(config.useWidth).toBe(850);
expect(config.useMaxWidth).toBeTruthy();
});
it('getConfig', () => {
expect(db.getConfig()).toStrictEqual(DEFAULT_PIE_DB.config);
});
it.todo('resetConfig', () => {
// db.setConfig({ textPosition: 0 });
// db.resetConfig();
expect(db.getConfig().textPosition).toStrictEqual(DEFAULT_PIE_DB.config.textPosition);
});
});
});

View File

@@ -1,69 +0,0 @@
import { log } from '../../logger.js';
import mermaidAPI from '../../mermaidAPI.js';
import * as configApi from '../../config.js';
import common from '../common/common.js';
import {
setAccTitle,
getAccTitle,
setDiagramTitle,
getDiagramTitle,
getAccDescription,
setAccDescription,
clear as commonClear,
} from '../../commonDb.js';
let sections = {};
let showData = false;
export const parseDirective = function (statement, context, type) {
mermaidAPI.parseDirective(this, statement, context, type);
};
const addSection = function (id, value) {
id = common.sanitizeText(id, configApi.getConfig());
if (sections[id] === undefined) {
sections[id] = value;
log.debug('Added new section :', id);
}
};
const getSections = () => sections;
const setShowData = function (toggle) {
showData = toggle;
};
const getShowData = function () {
return showData;
};
const cleanupValue = function (value) {
if (value.substring(0, 1) === ':') {
value = value.substring(1).trim();
return Number(value.trim());
} else {
return Number(value.trim());
}
};
const clear = function () {
sections = {};
showData = false;
commonClear();
};
export default {
parseDirective,
getConfig: () => configApi.getConfig().pie,
addSection,
getSections,
cleanupValue,
clear,
setAccTitle,
getAccTitle,
setDiagramTitle,
getDiagramTitle,
setShowData,
getShowData,
getAccDescription,
setAccDescription,
};

View File

@@ -0,0 +1,77 @@
import { log } from '../../logger.js';
import { getConfig as commonGetConfig } from '../../config.js';
import { sanitizeText } from '../common/common.js';
import {
setAccTitle,
getAccTitle,
setDiagramTitle,
getDiagramTitle,
getAccDescription,
setAccDescription,
clear as commonClear,
} from '../common/commonDb.js';
import type { PieFields, PieDB, Sections } from './pieTypes.js';
import type { RequiredDeep } from 'type-fest';
import type { PieDiagramConfig } from '../../config.type.js';
import DEFAULT_CONFIG from '../../defaultConfig.js';
export const DEFAULT_PIE_CONFIG: Required<PieDiagramConfig> = DEFAULT_CONFIG.pie;
export const DEFAULT_PIE_DB: RequiredDeep<PieFields> = {
sections: {},
showData: false,
config: DEFAULT_PIE_CONFIG,
} as const;
let sections: Sections = DEFAULT_PIE_DB.sections;
let showData: boolean = DEFAULT_PIE_DB.showData;
const config: Required<PieDiagramConfig> = structuredClone(DEFAULT_PIE_CONFIG);
const getConfig = (): Required<PieDiagramConfig> => structuredClone(config);
const clear = (): void => {
sections = structuredClone(DEFAULT_PIE_DB.sections);
showData = DEFAULT_PIE_DB.showData;
commonClear();
};
const addSection = (label: string, value: number): void => {
label = sanitizeText(label, commonGetConfig());
if (sections[label] === undefined) {
sections[label] = value;
log.debug(`added new section: ${label}, with value: ${value}`);
}
};
const getSections = (): Sections => sections;
const cleanupValue = (value: string): number => {
if (value.substring(0, 1) === ':') {
value = value.substring(1).trim();
}
return Number(value.trim());
};
const setShowData = (toggle: boolean): void => {
showData = toggle;
};
const getShowData = (): boolean => showData;
export const db: PieDB = {
getConfig,
clear,
setDiagramTitle,
getDiagramTitle,
setAccTitle,
getAccTitle,
setAccDescription,
getAccDescription,
addSection,
getSections,
cleanupValue,
setShowData,
getShowData,
};

View File

@@ -15,10 +15,8 @@ const loader: DiagramLoader = async () => {
return { id, diagram };
};
const plugin: ExternalDiagramDefinition = {
export const pie: ExternalDiagramDefinition = {
id,
detector,
loader,
};
export default plugin;

View File

@@ -1,9 +1,9 @@
import { DiagramDefinition } from '../../diagram-api/types.js';
import type { DiagramDefinition } from '../../diagram-api/types.js';
// @ts-ignore: JISON doesn't support types
import parser from './parser/pie.jison';
import db from './pieDb.js';
import styles from './styles.js';
import renderer from './pieRenderer.js';
import { db } from './pieDb.js';
import styles from './pieStyles.js';
import { renderer } from './pieRenderer.js';
export const diagram: DiagramDefinition = {
parser,

View File

@@ -1,204 +0,0 @@
/** Created by AshishJ on 11-09-2019. */
import { select, scaleOrdinal, pie as d3pie, arc } from 'd3';
import { log } from '../../logger.js';
import { configureSvgSize } from '../../setupGraphViewbox.js';
import * as configApi from '../../config.js';
import { parseFontSize } from '../../utils.js';
let conf = configApi.getConfig();
/**
* Draws a Pie Chart with the data given in text.
*
* @param text
* @param id
*/
let width;
const height = 450;
export const draw = (txt, id, _version, diagObj) => {
try {
conf = configApi.getConfig();
log.debug('Rendering info diagram\n' + txt);
const securityLevel = configApi.getConfig().securityLevel;
// Handle root and Document for when rendering in sandbox mode
let sandboxElement;
if (securityLevel === 'sandbox') {
sandboxElement = select('#i' + id);
}
const root =
securityLevel === 'sandbox'
? select(sandboxElement.nodes()[0].contentDocument.body)
: select('body');
const doc = securityLevel === 'sandbox' ? sandboxElement.nodes()[0].contentDocument : document;
// Parse the Pie Chart definition
const elem = doc.getElementById(id);
width = elem.parentElement.offsetWidth;
if (width === undefined) {
width = 1200;
}
if (conf.useWidth !== undefined) {
width = conf.useWidth;
}
if (conf.pie.useWidth !== undefined) {
width = conf.pie.useWidth;
}
const diagram = root.select('#' + id);
configureSvgSize(diagram, height, width, conf.pie.useMaxWidth);
// Set viewBox
elem.setAttribute('viewBox', '0 0 ' + width + ' ' + height);
// Fetch the default direction, use TD if none was found
var margin = 40;
var legendRectSize = 18;
var legendSpacing = 4;
var radius = Math.min(width, height) / 2 - margin;
var svg = diagram
.append('g')
.attr('transform', 'translate(' + width / 2 + ',' + height / 2 + ')');
var data = diagObj.db.getSections();
var sum = 0;
Object.keys(data).forEach(function (key) {
sum += data[key];
});
const themeVariables = conf.themeVariables;
var myGeneratedColors = [
themeVariables.pie1,
themeVariables.pie2,
themeVariables.pie3,
themeVariables.pie4,
themeVariables.pie5,
themeVariables.pie6,
themeVariables.pie7,
themeVariables.pie8,
themeVariables.pie9,
themeVariables.pie10,
themeVariables.pie11,
themeVariables.pie12,
];
const textPosition = conf.pie?.textPosition ?? 0.75;
let [outerStrokeWidth] = parseFontSize(themeVariables.pieOuterStrokeWidth);
outerStrokeWidth ??= 2;
// Set the color scale
var color = scaleOrdinal().range(myGeneratedColors);
// Compute the position of each group on the pie:
var pieData = Object.entries(data).map(function (el, idx) {
return {
order: idx,
name: el[0],
value: el[1],
};
});
var pie = d3pie()
.value(function (d) {
return d.value;
})
.sort(function (a, b) {
// Sort slices in clockwise direction
return a.order - b.order;
});
var dataReady = pie(pieData);
// Shape helper to build arcs:
var arcGenerator = arc().innerRadius(0).outerRadius(radius);
var labelArcGenerator = arc()
.innerRadius(radius * textPosition)
.outerRadius(radius * textPosition);
svg
.append('circle')
.attr('cx', 0)
.attr('cy', 0)
.attr('r', radius + outerStrokeWidth / 2)
.attr('class', 'pieOuterCircle');
// Build the pie chart: each part of the pie is a path that we build using the arc function.
svg
.selectAll('mySlices')
.data(dataReady)
.enter()
.append('path')
.attr('d', arcGenerator)
.attr('fill', function (d) {
return color(d.data.name);
})
.attr('class', 'pieCircle');
// Now add the percentage.
// Use the centroid method to get the best coordinates.
svg
.selectAll('mySlices')
.data(dataReady)
.enter()
.append('text')
.text(function (d) {
return ((d.data.value / sum) * 100).toFixed(0) + '%';
})
.attr('transform', function (d) {
return 'translate(' + labelArcGenerator.centroid(d) + ')';
})
.style('text-anchor', 'middle')
.attr('class', 'slice');
svg
.append('text')
.text(diagObj.db.getDiagramTitle())
.attr('x', 0)
.attr('y', -(height - 50) / 2)
.attr('class', 'pieTitleText');
// Add the legends/annotations for each section
var legend = svg
.selectAll('.legend')
.data(color.domain())
.enter()
.append('g')
.attr('class', 'legend')
.attr('transform', function (d, i) {
const height = legendRectSize + legendSpacing;
const offset = (height * color.domain().length) / 2;
const horizontal = 12 * legendRectSize;
const vertical = i * height - offset;
return 'translate(' + horizontal + ',' + vertical + ')';
});
legend
.append('rect')
.attr('width', legendRectSize)
.attr('height', legendRectSize)
.style('fill', color)
.style('stroke', color);
legend
.data(dataReady)
.append('text')
.attr('x', legendRectSize + legendSpacing)
.attr('y', legendRectSize - legendSpacing)
.text(function (d) {
if (diagObj.db.getShowData() || conf.showData || conf.pie.showData) {
return d.data.name + ' [' + d.data.value + ']';
} else {
return d.data.name;
}
});
} catch (e) {
log.error('Error while rendering info diagram');
log.error(e);
}
};
export default {
draw,
};

View File

@@ -0,0 +1,182 @@
import type d3 from 'd3';
import { scaleOrdinal, pie as d3pie, arc } from 'd3';
import { log } from '../../logger.js';
import { configureSvgSize } from '../../setupGraphViewbox.js';
import { getConfig } from '../../config.js';
import { cleanAndMerge, parseFontSize } from '../../utils.js';
import type { DrawDefinition, Group, SVG } from '../../diagram-api/types.js';
import type { D3Sections, PieDB, Sections } from './pieTypes.js';
import type { MermaidConfig, PieDiagramConfig } from '../../config.type.js';
import { selectSvgElement } from '../../rendering-util/selectSvgElement.js';
const createPieArcs = (sections: Sections): d3.PieArcDatum<D3Sections>[] => {
// Compute the position of each group on the pie:
const pieData: D3Sections[] = Object.entries(sections)
.map((element: [string, number]): D3Sections => {
return {
label: element[0],
value: element[1],
};
})
.sort((a: D3Sections, b: D3Sections): number => {
return b.value - a.value;
});
const pie: d3.Pie<unknown, D3Sections> = d3pie<D3Sections>().value(
(d3Section: D3Sections): number => d3Section.value
);
return pie(pieData);
};
/**
* Draws a Pie Chart with the data given in text.
*
* @param text - pie chart code
* @param id - diagram id
* @param _version - MermaidJS version from package.json.
* @param diagObj - A standard diagram containing the DB and the text and type etc of the diagram.
*/
export const draw: DrawDefinition = (text, id, _version, diagObj) => {
log.debug('rendering pie chart\n' + text);
const db = diagObj.db as PieDB;
const globalConfig: MermaidConfig = getConfig();
const pieConfig: Required<PieDiagramConfig> = cleanAndMerge(db.getConfig(), globalConfig.pie);
const height = 450;
// TODO: remove document width
const width: number =
document.getElementById(id)?.parentElement?.offsetWidth ?? pieConfig.useWidth;
const svg: SVG = selectSvgElement(id);
// Set viewBox
svg.attr('viewBox', `0 0 ${width} ${height}`);
configureSvgSize(svg, height, width, pieConfig.useMaxWidth);
const MARGIN = 40;
const LEGEND_RECT_SIZE = 18;
const LEGEND_SPACING = 4;
const group: Group = svg.append('g');
group.attr('transform', 'translate(' + width / 2 + ',' + height / 2 + ')');
const { themeVariables } = globalConfig;
let [outerStrokeWidth] = parseFontSize(themeVariables.pieOuterStrokeWidth);
outerStrokeWidth ??= 2;
const textPosition: number = pieConfig.textPosition;
const radius: number = Math.min(width, height) / 2 - MARGIN;
// Shape helper to build arcs:
const arcGenerator: d3.Arc<unknown, d3.PieArcDatum<D3Sections>> = arc<
d3.PieArcDatum<D3Sections>
>()
.innerRadius(0)
.outerRadius(radius);
const labelArcGenerator: d3.Arc<unknown, d3.PieArcDatum<D3Sections>> = arc<
d3.PieArcDatum<D3Sections>
>()
.innerRadius(radius * textPosition)
.outerRadius(radius * textPosition);
group
.append('circle')
.attr('cx', 0)
.attr('cy', 0)
.attr('r', radius + outerStrokeWidth / 2)
.attr('class', 'pieOuterCircle');
const sections: Sections = db.getSections();
const arcs: d3.PieArcDatum<D3Sections>[] = createPieArcs(sections);
const myGeneratedColors = [
themeVariables.pie1,
themeVariables.pie2,
themeVariables.pie3,
themeVariables.pie4,
themeVariables.pie5,
themeVariables.pie6,
themeVariables.pie7,
themeVariables.pie8,
themeVariables.pie9,
themeVariables.pie10,
themeVariables.pie11,
themeVariables.pie12,
];
// Set the color scale
const color: d3.ScaleOrdinal<string, 12, never> = scaleOrdinal(myGeneratedColors);
// Build the pie chart: each part of the pie is a path that we build using the arc function.
group
.selectAll('mySlices')
.data(arcs)
.enter()
.append('path')
.attr('d', arcGenerator)
.attr('fill', (datum: d3.PieArcDatum<D3Sections>) => {
return color(datum.data.label);
})
.attr('class', 'pieCircle');
let sum = 0;
Object.keys(sections).forEach((key: string): void => {
sum += sections[key];
});
// Now add the percentage.
// Use the centroid method to get the best coordinates.
group
.selectAll('mySlices')
.data(arcs)
.enter()
.append('text')
.text((datum: d3.PieArcDatum<D3Sections>): string => {
return ((datum.data.value / sum) * 100).toFixed(0) + '%';
})
.attr('transform', (datum: d3.PieArcDatum<D3Sections>): string => {
return 'translate(' + labelArcGenerator.centroid(datum) + ')';
})
.style('text-anchor', 'middle')
.attr('class', 'slice');
group
.append('text')
.text(db.getDiagramTitle())
.attr('x', 0)
.attr('y', -(height - 50) / 2)
.attr('class', 'pieTitleText');
// Add the legends/annotations for each section
const legend = group
.selectAll('.legend')
.data(color.domain())
.enter()
.append('g')
.attr('class', 'legend')
.attr('transform', (_datum, index: number): string => {
const height = LEGEND_RECT_SIZE + LEGEND_SPACING;
const offset = (height * color.domain().length) / 2;
const horizontal = 12 * LEGEND_RECT_SIZE;
const vertical = index * height - offset;
return 'translate(' + horizontal + ',' + vertical + ')';
});
legend
.append('rect')
.attr('width', LEGEND_RECT_SIZE)
.attr('height', LEGEND_RECT_SIZE)
.style('fill', color)
.style('stroke', color);
legend
.data(arcs)
.append('text')
.attr('x', LEGEND_RECT_SIZE + LEGEND_SPACING)
.attr('y', LEGEND_RECT_SIZE - LEGEND_SPACING)
.text((datum: d3.PieArcDatum<D3Sections>): string => {
const { label, value } = datum.data;
if (db.getShowData()) {
return `${label} [${value}]`;
}
return label;
});
};
export const renderer = { draw };

View File

@@ -1,4 +1,7 @@
const getStyles = (options) =>
import type { DiagramStylesProvider } from '../../diagram-api/types.js';
import type { PieStyleOptions } from './pieTypes.js';
const getStyles: DiagramStylesProvider = (options: PieStyleOptions) =>
`
.pieCircle{
stroke: ${options.pieStrokeColor};

View File

@@ -0,0 +1,63 @@
import type { PieDiagramConfig } from '../../config.type.js';
import type { DiagramDB } from '../../diagram-api/types.js';
export interface PieFields {
sections: Sections;
showData: boolean;
config: PieDiagramConfig;
}
export interface PieStyleOptions {
fontFamily: string;
pie1: string;
pie2: string;
pie3: string;
pie4: string;
pie5: string;
pie6: string;
pie7: string;
pie8: string;
pie9: string;
pie10: string;
pie11: string;
pie12: string;
pieTitleTextSize: string;
pieTitleTextColor: string;
pieSectionTextSize: string;
pieSectionTextColor: string;
pieLegendTextSize: string;
pieLegendTextColor: string;
pieStrokeColor: string;
pieStrokeWidth: string;
pieOuterStrokeWidth: string;
pieOuterStrokeColor: string;
pieOpacity: string;
}
export type Sections = Record<string, number>;
export interface D3Sections {
label: string;
value: number;
}
export interface PieDB extends DiagramDB {
// config
getConfig: () => Required<PieDiagramConfig>;
// common db
clear: () => void;
setDiagramTitle: (title: string) => void;
getDiagramTitle: () => string;
setAccTitle: (title: string) => void;
getAccTitle: () => string;
setAccDescription: (describetion: string) => void;
getAccDescription: () => string;
// diagram db
addSection: (label: string, value: number) => void;
getSections: () => Sections;
cleanupValue: (value: string) => number;
setShowData: (toggle: boolean) => void;
getShowData: () => boolean;
}

View File

@@ -5,10 +5,6 @@
%x string
%x md_string
%x title
%x open_directive
%x type_directive
%x arg_directive
%x close_directive
%x acc_title
%x acc_descr
%x acc_descr_multiline
@@ -16,11 +12,6 @@
%x point_x
%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\r]+ return 'NEWLINE';
@@ -87,7 +78,6 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multiline");}
start
: eol start
| SPACE start
| directive start
| QUADRANT document
;
@@ -110,7 +100,6 @@ statement
| acc_title acc_title_value { $$=$2.trim();yy.setAccTitle($$); }
| 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);}
| directive
;
points
@@ -133,33 +122,12 @@ quadrantDetails
| QUADRANT_4 text {yy.setQuadrant4Text($2)}
;
directive
: openDirective typeDirective closeDirective
| openDirective typeDirective ':' argDirective closeDirective
;
eol
: NEWLINE
| SEMI
| 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:$1, type: 'text'};}
| text textNoTagsToken

View File

@@ -1,6 +1,7 @@
// @ts-ignore: JISON doesn't support types
import { parser } from './quadrant.jison';
import { Mock, vi } from 'vitest';
import type { Mock } from 'vitest';
import { vi } from 'vitest';
const parserFnConstructor = (str: string) => {
return () => {
@@ -18,7 +19,6 @@ const mockDB: Record<string, Mock<any, any>> = {
setYAxisTopText: vi.fn(),
setYAxisBottomText: vi.fn(),
setDiagramTitle: vi.fn(),
parseDirective: vi.fn(),
addPoint: vi.fn(),
};
@@ -44,23 +44,6 @@ describe('Testing quadrantChart jison file', () => {
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', () => {
let str = 'quadrantChart\nx-axis urgent --> not urgent';
expect(parserFnConstructor(str)).not.toThrow();
@@ -242,8 +225,7 @@ describe('Testing quadrantChart jison file', () => {
});
it('should be able to parse the whole chart', () => {
const str = `%%{init: {"quadrantChart": {"chartWidth": 600, "chartHeight": 600} } }%%
quadrantChart
const str = `quadrantChart
title Analytics and Business Intelligence Platforms
x-axis "Completeness of Vision ❤" --> "x-axis-2"
y-axis Ability to Execute --> "y-axis-2"
@@ -257,17 +239,6 @@ describe('Testing quadrantChart jison file', () => {
Incorta: [0.20, 0.30]`;
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({
text: 'Completeness of Vision ❤',
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 { sanitizeText } from '../common/common.js';
import {
@@ -10,7 +8,7 @@ import {
getAccDescription,
setAccDescription,
clear as commonClear,
} from '../../commonDb.js';
} from '../common/commonDb.js';
import { QuadrantBuilder } from './quadrantBuilder.js';
const config = configApi.getConfig();
@@ -94,11 +92,6 @@ function getQuadrantData() {
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 () {
quadrantBuilder.clear();
commonClear();
@@ -117,7 +110,6 @@ export default {
setYAxisBottomText,
addPoint,
getQuadrantData,
parseDirective,
clear,
setAccTitle,
getAccTitle,

View File

@@ -1,4 +1,4 @@
import { DiagramDefinition } from '../../diagram-api/types.js';
import type { DiagramDefinition } from '../../diagram-api/types.js';
// @ts-ignore: JISON doesn't support types
import parser from './parser/quadrant.jison';
import db from './quadrantDb.js';

View File

@@ -3,8 +3,8 @@ import { select } from 'd3';
import * as configApi from '../../config.js';
import { log } from '../../logger.js';
import { configureSvgSize } from '../../setupGraphViewbox.js';
import { Diagram } from '../../Diagram.js';
import {
import type { Diagram } from '../../Diagram.js';
import type {
QuadrantBuildType,
QuadrantLineType,
QuadrantPointType,

View File

@@ -9,19 +9,10 @@
%x string
%x token
%x unqString
%x open_directive
%x type_directive
%x arg_directive
%x close_directive
%x acc_title
%x acc_descr
%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';
accTitle\s*":"\s* { this.begin("acc_title");return 'acc_title'; }
@@ -99,23 +90,10 @@ start
| RD NEWLINE diagram EOF;
directive
: openDirective typeDirective closeDirective
| openDirective typeDirective ':' argDirective closeDirective
| 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_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
: /* empty */ { $$ = [] }

View File

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

View File

@@ -1,4 +1,4 @@
import { DiagramDefinition } from '../../diagram-api/types.js';
import type { DiagramDefinition } from '../../diagram-api/types.js';
// @ts-ignore: JISON doesn't support types
import parser from './parser/requirementDiagram.jison';
import db from './requirementDb.js';

View File

@@ -8,7 +8,7 @@ import {
setDiagramTitle,
getDiagramTitle,
clear as commonClear,
} from '../../commonDb.js';
} from '../common/commonDb.js';
// Sankey diagram represented by nodes and links between those nodes
let links: SankeyLink[] = [];
@@ -31,7 +31,7 @@ class SankeyLink {
/**
* @param source - Node where the link starts
* @param target - Node where the link ends
* @param value - number, float or integer, describes the amount to be passed
* @param value - Describes the amount to be passed
*/
const addLink = (source: SankeyNode, target: SankeyNode, value: number): void => {
links.push(new SankeyLink(source, target, value));

View File

@@ -1,4 +1,4 @@
import { DiagramDefinition } from '../../diagram-api/types.js';
import type { DiagramDefinition } from '../../diagram-api/types.js';
// @ts-ignore: jison doesn't export types
import parser from './parser/sankey.jison';
import db from './sankeyDB.js';

View File

@@ -1,4 +1,4 @@
import { Diagram } from '../../Diagram.js';
import type { Diagram } from '../../Diagram.js';
import * as configApi from '../../config.js';
import {
@@ -7,6 +7,7 @@ import {
schemeTableau10 as d3schemeTableau10,
} from 'd3';
import type { SankeyNode as d3SankeyNode } from 'd3-sankey';
import {
sankey as d3Sankey,
sankeyLinkHorizontal as d3SankeyLinkHorizontal,
@@ -14,11 +15,10 @@ import {
sankeyRight as d3SankeyRight,
sankeyCenter as d3SankeyCenter,
sankeyJustify as d3SankeyJustify,
SankeyNode as d3SankeyNode,
} from 'd3-sankey';
import { configureSvgSize } from '../../setupGraphViewbox.js';
import { Uid } from '../../rendering-util/uid.js';
import type { SankeyLinkColor, SankeyNodeAlignment } from '../../config.type.js';
import type { SankeyNodeAlignment } from '../../config.type.js';
// Map config options to alignment functions
const alignmentsMap: Record<
@@ -62,10 +62,13 @@ export const draw = function (text: string, id: string, _version: string, diagOb
// Establish svg dimensions and get width and height
//
const width = conf?.width || defaultSankeyConfig.width!;
const height = conf?.height || defaultSankeyConfig.width!;
const useMaxWidth = conf?.useMaxWidth || defaultSankeyConfig.useMaxWidth!;
const nodeAlignment = conf?.nodeAlignment || defaultSankeyConfig.nodeAlignment!;
const width = conf?.width ?? defaultSankeyConfig.width!;
const height = conf?.height ?? defaultSankeyConfig.width!;
const useMaxWidth = conf?.useMaxWidth ?? defaultSankeyConfig.useMaxWidth!;
const nodeAlignment = conf?.nodeAlignment ?? defaultSankeyConfig.nodeAlignment!;
const prefix = conf?.prefix ?? defaultSankeyConfig.prefix!;
const suffix = conf?.suffix ?? defaultSankeyConfig.suffix!;
const showValues = conf?.showValues ?? defaultSankeyConfig.showValues!;
// FIX: using max width prevents height from being set, is it intended?
// to add height directly one can use `svg.attr('height', height)`
@@ -94,7 +97,7 @@ export const draw = function (text: string, id: string, _version: string, diagOb
const sankey = d3Sankey()
.nodeId((d: any) => d.id) // we use 'id' property to identify node
.nodeWidth(nodeWidth)
.nodePadding(10)
.nodePadding(10 + (showValues ? 15 : 0))
.nodeAlign(nodeAlign)
.extent([
[0, 0],
@@ -130,6 +133,13 @@ export const draw = function (text: string, id: string, _version: string, diagOb
.attr('width', (d: any) => d.x1 - d.x0)
.attr('fill', (d: any) => colorScheme(d.id));
const getText = ({ id, value }: { id: string; value: number }) => {
if (!showValues) {
return id;
}
return `${id}\n${prefix}${Math.round(value * 100) / 100}${suffix}`;
};
// Create labels for nodes
svg
.append('g')
@@ -141,9 +151,9 @@ export const draw = function (text: string, id: string, _version: string, diagOb
.join('text')
.attr('x', (d: any) => (d.x0 < width / 2 ? d.x1 + 6 : d.x0 - 6))
.attr('y', (d: any) => (d.y1 + d.y0) / 2)
.attr('dy', '0.35em')
.attr('dy', `${showValues ? '0' : '0.35'}em`)
.attr('text-anchor', (d: any) => (d.x0 < width / 2 ? 'start' : 'end'))
.text((d: any) => d.id);
.text(getText);
// Creates the paths that represent the links.
const link = svg

View File

@@ -16,22 +16,15 @@
// A special state for grabbing text up to the first comment/newline
%x ID ALIAS LINE
// Directive states
%x open_directive type_directive arg_directive
%x acc_title
%x acc_descr
%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';
\s+ /* skip all 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 */
[0-9]+(?=[ \n]+) return 'NUM';
@@ -106,7 +99,6 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multili
start
: SPACE start
| NEWLINE start
| directive start
| SD document { yy.apply($2);return $2; }
;
@@ -133,11 +125,6 @@ box_line
;
directive
: openDirective typeDirective closeDirective 'NEWLINE'
| openDirective typeDirective ':' argDirective closeDirective 'NEWLINE'
;
statement
: participant_statement
| '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.push({type: 'breakEnd', optText:yy.parseMessage($2), signalType: yy.LINETYPE.BREAK_END});
$$=$3;}
| directive
;
option_sections
@@ -301,7 +287,7 @@ placement
signal
: actor signaltype '+' actor text2
{ $$ = [$1,$4,{type: 'addMessage', from:$1.actor, to:$4.actor, signalType:$2, msg:$5},
{ $$ = [$1,$4,{type: 'addMessage', from:$1.actor, to:$4.actor, signalType:$2, msg:$5, activate: true},
{type: 'activeStart', signalType: yy.LINETYPE.ACTIVE_START, actor: $4}
]}
| actor signaltype '-' actor text2
@@ -335,20 +321,4 @@ text2
: 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 { log } from '../../logger.js';
import { sanitizeText } from '../common/common.js';
@@ -10,7 +9,7 @@ import {
getAccDescription,
setAccDescription,
clear as commonClear,
} from '../../commonDb.js';
} from '../common/commonDb.js';
let prevActor = undefined;
let actors = {};
@@ -25,10 +24,6 @@ let currentBox = undefined;
let lastCreated = undefined;
let lastDestroyed = undefined;
export const parseDirective = function (statement, context, type) {
mermaidAPI.parseDirective(this, statement, context, type);
};
export const addBox = function (data) {
boxes.push({
name: data.text,
@@ -124,7 +119,8 @@ export const addSignal = function (
idFrom,
idTo,
message = { text: undefined, wrap: undefined },
messageType
messageType,
activate = false
) {
if (messageType === LINETYPE.ACTIVE_END) {
const cnt = activationCount(idFrom.actor);
@@ -147,6 +143,7 @@ export const addSignal = function (
message: message.text,
wrap: (message.wrap === undefined && autoWrap()) || !!message.wrap,
type: messageType,
activate,
});
return true;
};
@@ -450,6 +447,19 @@ export const getActorProperty = function (actor, key) {
return undefined;
};
/**
* @typedef {object} AddMessageParams A message from one actor to another.
* @property {string} from - The id of the actor sending the message.
* @property {string} to - The id of the actor receiving the message.
* @property {string} msg - The message text.
* @property {number} signalType - The type of signal.
* @property {"addMessage"} type - Set to `"addMessage"` if this is an `AddMessageParams`.
* @property {boolean} [activate] - If `true`, this signal starts an activation.
*/
/**
* @param {object | object[] | AddMessageParams} param - Object of parameters.
*/
export const apply = function (param) {
if (Array.isArray(param)) {
param.forEach(function (item) {
@@ -530,7 +540,7 @@ export const apply = function (param) {
lastDestroyed = undefined;
}
}
addSignal(param.from, param.to, param.msg, param.signalType);
addSignal(param.from, param.to, param.msg, param.signalType, param.activate);
break;
case 'boxStart':
addBox(param.boxData);
@@ -619,7 +629,6 @@ export default {
getBoxes,
getDiagramTitle,
setDiagramTitle,
parseDirective,
getConfig: () => configApi.getConfig().sequence,
clear,
parseMessage,

View File

@@ -1,5 +1,4 @@
import { vi } from 'vitest';
import * as configApi from '../../config.js';
import mermaidAPI from '../../mermaidAPI.js';
import { Diagram, getDiagramFromText } from '../../Diagram.js';
@@ -104,6 +103,7 @@ describe('more than one sequence diagram', () => {
expect(diagram1.db.getMessages()).toMatchInlineSnapshot(`
[
{
"activate": false,
"from": "Alice",
"message": "Hello Bob, how are you?",
"to": "Bob",
@@ -111,6 +111,7 @@ describe('more than one sequence diagram', () => {
"wrap": false,
},
{
"activate": false,
"from": "Bob",
"message": "I am good thanks!",
"to": "Alice",
@@ -127,6 +128,7 @@ describe('more than one sequence diagram', () => {
expect(diagram2.db.getMessages()).toMatchInlineSnapshot(`
[
{
"activate": false,
"from": "Alice",
"message": "Hello Bob, how are you?",
"to": "Bob",
@@ -134,6 +136,7 @@ describe('more than one sequence diagram', () => {
"wrap": false,
},
{
"activate": false,
"from": "Bob",
"message": "I am good thanks!",
"to": "Alice",
@@ -152,6 +155,7 @@ describe('more than one sequence diagram', () => {
expect(diagram3.db.getMessages()).toMatchInlineSnapshot(`
[
{
"activate": false,
"from": "Alice",
"message": "Hello John, how are you?",
"to": "John",
@@ -159,6 +163,7 @@ describe('more than one sequence diagram', () => {
"wrap": false,
},
{
"activate": false,
"from": "John",
"message": "I am good thanks!",
"to": "Alice",
@@ -219,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
expect(diagram.db.showSequenceNumbers()).toBe(true);
});
it('should handle a sequenceDiagram definition with a title:', async () => {
const str = `
sequenceDiagram
@@ -548,6 +554,7 @@ deactivate Bob`;
expect(messages.length).toBe(4);
expect(messages[0].type).toBe(diagram.db.LINETYPE.DOTTED);
expect(messages[0].activate).toBeTruthy();
expect(messages[1].type).toBe(diagram.db.LINETYPE.ACTIVE_START);
expect(messages[1].from.actor).toBe('Bob');
expect(messages[2].type).toBe(diagram.db.LINETYPE.DOTTED);
@@ -2027,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

@@ -1,4 +1,4 @@
import { DiagramDefinition } from '../../diagram-api/types.js';
import type { DiagramDefinition } from '../../diagram-api/types.js';
// @ts-ignore: JISON doesn't support types
import parser from './parser/sequenceDiagram.jison';
import db from './sequenceDb.js';
@@ -10,4 +10,7 @@ export const diagram: DiagramDefinition = {
db,
renderer,
styles,
init: ({ wrap }) => {
db.setWrap(wrap);
},
};

View File

@@ -1,5 +1,5 @@
// @ts-nocheck TODO: fix file
import { select, selectAll } from 'd3';
import { select } from 'd3';
import svgDraw, { ACTOR_TYPE_WIDTH, drawText, fixLifeLineHeights } from './svgDraw.js';
import { log } from '../../logger.js';
import common from '../common/common.js';
@@ -8,7 +8,7 @@ import * as configApi from '../../config.js';
import assignWithDepth from '../../assignWithDepth.js';
import utils from '../../utils.js';
import { configureSvgSize } from '../../setupGraphViewbox.js';
import { Diagram } from '../../Diagram.js';
import type { Diagram } from '../../Diagram.js';
let conf = {};
@@ -622,10 +622,10 @@ const activationBounds = function (actor, actors) {
const left = activations.reduce(function (acc, activation) {
return common.getMin(acc, activation.startx);
}, actorObj.x + actorObj.width / 2);
}, actorObj.x + actorObj.width / 2 - 1);
const right = activations.reduce(function (acc, activation) {
return common.getMax(acc, activation.stopx);
}, actorObj.x + actorObj.width / 2);
}, actorObj.x + actorObj.width / 2 + 1);
return [left, right];
};
@@ -1389,9 +1389,8 @@ const buildNoteModel = function (msg, actors, diagObj) {
};
const buildMessageModel = function (msg, actors, diagObj) {
let process = false;
if (
[
![
diagObj.db.LINETYPE.SOLID_OPEN,
diagObj.db.LINETYPE.DOTTED_OPEN,
diagObj.db.LINETYPE.SOLID,
@@ -1402,17 +1401,47 @@ const buildMessageModel = function (msg, actors, diagObj) {
diagObj.db.LINETYPE.DOTTED_POINT,
].includes(msg.type)
) {
process = true;
}
if (!process) {
return {};
}
const fromBounds = activationBounds(msg.from, actors);
const toBounds = activationBounds(msg.to, actors);
const fromIdx = fromBounds[0] <= toBounds[0] ? 1 : 0;
const toIdx = fromBounds[0] < toBounds[0] ? 0 : 1;
const allBounds = [...fromBounds, ...toBounds];
const boundedWidth = Math.abs(toBounds[toIdx] - fromBounds[fromIdx]);
const [fromLeft, fromRight] = activationBounds(msg.from, actors);
const [toLeft, toRight] = activationBounds(msg.to, actors);
const isArrowToRight = fromLeft <= toLeft;
const startx = isArrowToRight ? fromRight : fromLeft;
let stopx = isArrowToRight ? toLeft : toRight;
// As the line width is considered, the left and right values will be off by 2.
const isArrowToActivation = Math.abs(toLeft - toRight) > 2;
/**
* Adjust the value based on the arrow direction
* @param value - The value to adjust
* @returns The adjustment with correct sign to be added to the actual value.
*/
const adjustValue = (value: number) => {
return isArrowToRight ? -value : value;
};
/**
* This is an edge case for the first activation.
* Proper fix would require significant changes.
* So, we set an activate flag in the message, and cross check that with isToActivation
* In cases where the message is to an activation that was properly detected, we don't want to move the arrow head
* The activation will not be detected on the first message, so we need to move the arrow head
*/
if (msg.activate && !isArrowToActivation) {
stopx += adjustValue(conf.activationWidth / 2 - 1);
}
/**
* Shorten the length of arrow at the end and move the marker forward (using refX) to have a clean arrowhead
* This is not required for open arrows that don't have arrowheads
*/
if (![diagObj.db.LINETYPE.SOLID_OPEN, diagObj.db.LINETYPE.DOTTED_OPEN].includes(msg.type)) {
stopx += adjustValue(3);
}
const allBounds = [fromLeft, fromRight, toLeft, toRight];
const boundedWidth = Math.abs(startx - stopx);
if (msg.wrap && msg.message) {
msg.message = utils.wrapLabel(
msg.message,
@@ -1429,8 +1458,8 @@ const buildMessageModel = function (msg, actors, diagObj) {
conf.width
),
height: 0,
startx: fromBounds[fromIdx],
stopx: toBounds[toIdx],
startx,
stopx,
starty: 0,
stopy: 0,
message: msg.message,

View File

@@ -703,7 +703,7 @@ export const insertArrowHead = function (elem) {
.append('defs')
.append('marker')
.attr('id', 'arrowhead')
.attr('refX', 9)
.attr('refX', 7.9)
.attr('refY', 5)
.attr('markerUnits', 'userSpaceOnUse')
.attr('markerWidth', 12)
@@ -723,7 +723,7 @@ export const insertArrowFilledHead = function (elem) {
.append('defs')
.append('marker')
.attr('id', 'filled-head')
.attr('refX', 18)
.attr('refX', 15.5)
.attr('refY', 7)
.attr('markerWidth', 20)
.attr('markerHeight', 28)
@@ -768,7 +768,7 @@ export const insertArrowCrossHead = function (elem) {
.attr('markerHeight', 8)
.attr('orient', 'auto')
.attr('refX', 4)
.attr('refY', 5);
.attr('refY', 4.5);
// The cross
marker
.append('path')

View File

@@ -33,10 +33,6 @@
%x FLOATING_NOTE
%x FLOATING_NOTE_ID
%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
%x LINE
@@ -50,18 +46,13 @@
.*direction\s+RL[^\n]* return 'direction_rl';
.*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 */{ /*console.log('Crap after close');*/ }
[\n]+ return 'NL';
[\s]+ /* skip all whitespace */
<ID,STATE,struct,LINE,open_directive,type_directive,arg_directive,close_directive>((?!\n)\s)+ /* skip same-line whitespace */
<INITIAL,ID,STATE,struct,LINE,open_directive,type_directive,arg_directive,close_directive>\#[^\n]* /* skip comments */
<ID,STATE,struct,LINE>((?!\n)\s)+ /* skip same-line whitespace */
<INITIAL,ID,STATE,struct,LINE>\#[^\n]* /* skip comments */
\%%[^\n]* /* skip comments */
"scale"\s+ { this.pushState('SCALE'); /* console.log('Got scale', yytext);*/ return 'scale'; }
<SCALE>\d+ return 'WIDTH';
@@ -155,7 +146,6 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multili
start
: SPACE start
| NL start
| directive start
| 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()}};
}
| note NOTE_TEXT AS ID
| directive
| direction
| acc_title acc_title_value { $$=$2.trim();yy.setAccTitle($$); }
| acc_descr acc_descr_value { $$=$2.trim();yy.setAccDescription($$); }
@@ -264,10 +253,6 @@ cssClassStatement
}
;
directive
: openDirective typeDirective closeDirective
| openDirective typeDirective ':' argDirective closeDirective
;
direction
: direction_tb
{ yy.setDirection('TB');$$={stmt:'dir', value:'TB'};}
@@ -308,20 +293,4 @@ notePosition
| 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 { generateId } from '../../utils.js';
import mermaidAPI from '../../mermaidAPI.js';
import common from '../common/common.js';
import * as configApi from '../../config.js';
import {
@@ -11,7 +10,7 @@ import {
clear as commonClear,
setDiagramTitle,
getDiagramTitle,
} from '../../commonDb.js';
} from '../common/commonDb.js';
import {
DEFAULT_DIAGRAM_DIRECTION,
@@ -77,10 +76,6 @@ export const relationType = {
const clone = (o) => JSON.parse(JSON.stringify(o));
export const parseDirective = function (statement, context, type) {
mermaidAPI.parseDirective(this, statement, context, type);
};
const setRootDoc = (o) => {
log.info('Setting 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());
export default {
parseDirective,
getConfig: () => configApi.getConfig().state,
addState,
clear,

View File

@@ -55,16 +55,6 @@ describe('state diagram V2, ', function () {
const title = stateDb.getAccTitle();
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 () {
const str = `stateDiagram-v2\n
[*] --> State1

View File

@@ -1,4 +1,4 @@
import { DiagramDefinition } from '../../diagram-api/types.js';
import type { DiagramDefinition } from '../../diagram-api/types.js';
// @ts-ignore: JISON doesn't support types
import parser from './parser/stateDiagram.jison';
import db from './stateDb.js';

View File

@@ -66,16 +66,6 @@ describe('state diagram, ', function () {
const title = stateDb.getAccTitle();
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 () {
const str = `stateDiagram\n
[*] --> State1

View File

@@ -1,4 +1,4 @@
import { DiagramDefinition } from '../../diagram-api/types.js';
import type { DiagramDefinition } from '../../diagram-api/types.js';
// @ts-ignore: JISON doesn't support types
import parser from './parser/stateDiagram.jison';
import db from './stateDb.js';

View File

@@ -9,17 +9,8 @@
%x acc_descr
%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]+ return 'NEWLINE';
@@ -55,7 +46,6 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multili
start
: timeline document 'EOF' { return $2; }
| directive start
;
document
@@ -70,11 +60,6 @@ line
| EOF { $$=[];}
;
directive
: openDirective typeDirective closeDirective 'NEWLINE'
| openDirective typeDirective ':' argDirective closeDirective 'NEWLINE'
;
statement
: title {yy.getCommonDb().setDiagramTitle($1.substr(6));$$=$1.substr(6);}
| acc_title acc_title_value { $$=$2.trim();yy.getCommonDb().setAccTitle($$); }
@@ -83,7 +68,6 @@ statement
| section {yy.addSection($1.substr(8));$$=$1.substr(8);}
| period_statement
| event_statement
| directive
;
period_statement
: period {yy.addTask($1,0,'');$$=$1;}
@@ -92,21 +76,4 @@ event_statement
: 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,26 +1,6 @@
import { parser as timeline } from './parser/timeline.jison';
import * as timelineDB from './timelineDb.js';
// import { injectUtils } from './mermaidUtils.js';
import * as _commonDb from '../../commonDb.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,
// _commonDb,
// _parseDirective
// );
import { setLogLevel } from '../../diagram-api/diagramAPI.js';
describe('when parsing a timeline ', function () {
beforeEach(function () {

View File

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

Some files were not shown because too many files have changed in this diff Show More