Merge branch 'sidv/biome' into sidv/5237-unified-with-biome

* sidv/biome: (63 commits)
  chore: Remove redundant eslint comments
  chore: Add `eslint-config-biome`
  chore: update browsers list
  chore(deps): update all patch dependencies
  docs: Added demo diagram of bidirectional arrows for sequence diagrams
  Format on pre-commit
  fix(deps): update all patch dependencies
  chore: Update drupal regex
  chore: Update pnpm
  chore: Fix gantt test
  chore: Fix shape names
  chore: remove comments in json
  chore: Add eslint-disable
  chore: Add eslint-disable
  Fix prettier formatting on generation.
  chore: Add .eslintignore
  chore: Enable eslint in CI
  chore: Add biome renovate config
  chore: Remove unused deps
  chore: Update lint scripts
  ...
This commit is contained in:
Sidharth Vinod
2024-06-24 15:16:00 +05:30
98 changed files with 3943 additions and 2186 deletions

View File

@@ -12,12 +12,7 @@
},
"./*": "./*"
},
"keywords": [
"diagram",
"markdown",
"example",
"mermaid"
],
"keywords": ["diagram", "markdown", "example", "mermaid"],
"scripts": {
"prepublishOnly": "pnpm -w run build"
},
@@ -28,14 +23,8 @@
"author": "Knut Sveidqvist",
"license": "MIT",
"standard": {
"ignore": [
"**/parser/*.js",
"dist/**/*.js",
"cypress/**/*.js"
],
"globals": [
"page"
]
"ignore": ["**/parser/*.js", "dist/**/*.js", "cypress/**/*.js"],
"globals": ["page"]
},
"dependencies": {
"@braintree/sanitize-url": "^7.0.0",
@@ -47,11 +36,6 @@
"mermaid": "workspace:*",
"rimraf": "^5.0.5"
},
"files": [
"dist"
],
"sideEffects": [
"**/*.css",
"**/*.scss"
]
"files": ["dist"],
"sideEffects": ["**/*.css", "**/*.scss"]
}

View File

@@ -13,7 +13,7 @@ export const draw = (text, id, version) => {
try {
const conf = getConfig();
log.debug('Rendering example diagram\n' + text, 'Conf: ');
const THEME_COLOR_LIMIT = getConfig().themeVariables.THEME_COLOR_LIMIT;
const themeColorLimit = getConfig().themeVariables.THEME_COLOR_LIMIT;
const securityLevel = getConfig().securityLevel;
// Handle root and Document for when rendering in sandbox mode
let sandboxElement;
@@ -30,7 +30,7 @@ export const draw = (text, id, version) => {
const g = svg.append('g');
let i;
for (i = 0; i < THEME_COLOR_LIMIT; i++) {
for (i = 0; i < themeColorLimit; i++) {
const section = g.append('g').attr('class', 'section-' + i);
section
.append('rect')

View File

@@ -12,13 +12,7 @@
},
"./*": "./*"
},
"keywords": [
"diagram",
"markdown",
"flowchart",
"elk",
"mermaid"
],
"keywords": ["diagram", "markdown", "flowchart", "elk", "mermaid"],
"scripts": {
"prepublishOnly": "pnpm -w run build"
},
@@ -39,7 +33,5 @@
"mermaid": "workspace:^",
"rimraf": "^5.0.5"
},
"files": [
"dist"
]
"files": ["dist"]
}

View File

@@ -39,6 +39,26 @@ describe('flowchart-elk detector', () => {
).toBe(true);
});
// The error from the issue was reproduced with mindmap, so this is just an example
// what matters is the keyword somewhere inside graph definition
it('should check only the beginning of the line in search of keywords', () => {
expect(
detector('mindmap ["Descendant node in flowchart"]', {
flowchart: {
defaultRenderer: 'elk',
},
})
).toBe(false);
expect(
detector('mindmap ["Descendant node in graph"]', {
flowchart: {
defaultRenderer: 'elk',
},
})
).toBe(false);
});
it('should detect flowchart-elk', () => {
expect(detector('flowchart-elk')).toBe(true);
});

View File

@@ -11,7 +11,7 @@ const detector: DiagramDetector = (txt, config): boolean => {
// If diagram explicitly states flowchart-elk
/^\s*flowchart-elk/.test(txt) ||
// If a flowchart/graph diagram has their default renderer set to elk
(/^\s*flowchart|graph/.test(txt) && config?.flowchart?.defaultRenderer === 'elk')
(/^\s*(flowchart|graph)/.test(txt) && config?.flowchart?.defaultRenderer === 'elk')
) {
return true;
}

View File

@@ -444,18 +444,20 @@ export const addEdges = function (edges, diagObj, graph, svg) {
edgeData.arrowTypeStart = 'arrow_open';
edgeData.arrowTypeEnd = 'arrow_open';
/* eslint-disable no-fallthrough */
switch (edge.type) {
// biome-ignore lint/suspicious/noFallthroughSwitchClause: Expected fallthrough
case 'double_arrow_cross':
edgeData.arrowTypeStart = 'arrow_cross';
case 'arrow_cross':
edgeData.arrowTypeEnd = 'arrow_cross';
break;
// biome-ignore lint/suspicious/noFallthroughSwitchClause: Expected fallthrough
case 'double_arrow_point':
edgeData.arrowTypeStart = 'arrow_point';
case 'arrow_point':
edgeData.arrowTypeEnd = 'arrow_point';
break;
// biome-ignore lint/suspicious/noFallthroughSwitchClause: Expected fallthrough
case 'double_arrow_circle':
edgeData.arrowTypeStart = 'arrow_circle';
case 'arrow_circle':

View File

@@ -12,12 +12,7 @@
},
"./*": "./*"
},
"keywords": [
"diagram",
"markdown",
"zenuml",
"mermaid"
],
"keywords": ["diagram", "markdown", "zenuml", "mermaid"],
"scripts": {
"clean": "rimraf dist",
"prepublishOnly": "pnpm -w run build"
@@ -42,7 +37,5 @@
"peerDependencies": {
"mermaid": "workspace:>=10.0.0"
},
"files": [
"dist"
]
"files": ["dist"]
}

View File

@@ -58,19 +58,13 @@
"author": "Knut Sveidqvist",
"license": "MIT",
"standard": {
"ignore": [
"**/parser/*.js",
"dist/**/*.js",
"cypress/**/*.js"
],
"globals": [
"page"
]
"ignore": ["**/parser/*.js", "dist/**/*.js", "cypress/**/*.js"],
"globals": ["page"]
},
"dependencies": {
"@braintree/sanitize-url": "^7.0.1",
"@mermaid-js/parser": "workspace:^",
"cytoscape": "^3.28.1",
"cytoscape": "^3.29.2",
"cytoscape-cose-bilkent": "^4.1.0",
"d3": "^7.9.0",
"d3-sankey": "^0.12.3",
@@ -88,7 +82,7 @@
},
"devDependencies": {
"@adobe/jsonschema2md": "^8.0.0",
"@types/cytoscape": "^3.19.16",
"@types/cytoscape": "^3.21.4",
"@types/d3": "^7.4.3",
"@types/d3-sankey": "^0.12.4",
"@types/d3-scale": "^4.0.8",
@@ -103,8 +97,6 @@
"@types/prettier": "^3.0.0",
"@types/stylis": "^4.2.5",
"@types/uuid": "^9.0.8",
"@typescript-eslint/eslint-plugin": "^7.3.1",
"@typescript-eslint/parser": "^7.3.1",
"ajv": "^8.12.0",
"chokidar": "^3.6.0",
"concurrently": "^8.2.2",
@@ -132,10 +124,7 @@
"vitepress": "^1.0.1",
"vitepress-plugin-search": "1.0.4-alpha.22"
},
"files": [
"dist/",
"README.md"
],
"files": ["dist/", "README.md"],
"sideEffects": false,
"publishConfig": {
"access": "public"

View File

@@ -18,7 +18,7 @@ import { execFile } from 'node:child_process';
import { readFile, writeFile } from 'node:fs/promises';
import { join } from 'node:path';
import { promisify } from 'node:util';
import prettier from 'prettier';
import { prettierConfig } from './prettier.js';
// Workaround for wrong AJV types, see
// https://github.com/ajv-validator/ajv/issues/2132#issuecomment-1290409907
@@ -141,7 +141,7 @@ async function generateTypescript(mermaidConfigSchema: JSONSchemaType<MermaidCon
{
additionalProperties: false, // in JSON Schema 2019-09, these are called `unevaluatedProperties`
unreachableDefinitions: true, // definition for FontConfig is unreachable
style: (await prettier.resolveConfig('.')) ?? {},
style: prettierConfig,
}
);

View File

@@ -52,6 +52,7 @@ import mm from 'micromatch';
// @ts-ignore No typescript declaration file
import flatmap from 'unist-util-flatmap';
import { visit } from 'unist-util-visit';
import { prettierConfig } from './prettier.js';
export const MERMAID_RELEASE_VERSION = JSON.parse(readFileSync('../mermaid/package.json', 'utf8'))
.version as string;
@@ -85,7 +86,6 @@ const LOGMSG_COPIED = `, and copied to ${FINAL_DOCS_DIR}`;
const WARN_DOCSDIR_DOESNT_MATCH = `Changed files were transformed in ${SOURCE_DOCS_DIR} but do not match the files in ${FINAL_DOCS_DIR}. Please run 'pnpm --filter mermaid run docs:build' after making changes to ${SOURCE_DOCS_DIR} to update the ${FINAL_DOCS_DIR} directory with the transformed files.`;
const prettierConfig = (await prettier.resolveConfig('.')) ?? {};
// From https://github.com/vuejs/vitepress/blob/428eec3750d6b5648a77ac52d88128df0554d4d1/src/node/markdownToVue.ts#L20-L21
const includesRE = /<!--\s*@include:\s*(.*?)\s*-->/g;
const includedFiles: Set<string> = new Set();
@@ -367,26 +367,26 @@ async function transformJsonSchema(file: string) {
});
/** Location of the `schema.yaml` files */
const SCHEMA_INPUT_DIR = 'src/schemas/';
const schemaInputDir = 'src/schemas/';
/**
* Location to store the generated `schema.json` file for the website
*
* Because Vitepress doesn't handle bundling `.json` files properly, we need
* to instead place it into a `public/` subdirectory.
*/
const SCHEMA_OUTPUT_DIR = 'src/docs/public/schemas/';
const VITEPRESS_PUBLIC_DIR = 'src/docs/public';
const schemaOutputDir = 'src/docs/public/schemas/';
const vitepressPublicDir = 'src/docs/public';
/**
* Location to store the generated Schema Markdown docs.
* Links to JSON Schemas should automatically be rewritten to point to
* `SCHEMA_OUTPUT_DIR`.
*/
const SCHEMA_MARKDOWN_OUTPUT_DIR = join('src', 'docs', 'config', 'schema-docs');
const schemaMarkdownOutputDir = join('src', 'docs', 'config', 'schema-docs');
// write .schema.json files
const jsonFileName = file
.replace('.schema.yaml', '.schema.json')
.replace(SCHEMA_INPUT_DIR, SCHEMA_OUTPUT_DIR);
.replace(schemaInputDir, schemaOutputDir);
copyTransformedContents(jsonFileName, !verifyOnly, JSON.stringify(jsonSchema, undefined, 2));
const schemas = traverseSchemas([schemaLoader()(jsonFileName, jsonSchema)]);
@@ -414,7 +414,7 @@ async function transformJsonSchema(file: string) {
* @returns New absolute Vitepress path to schema
*/
rewritelinks: (origin: string) => {
return `/${relative(VITEPRESS_PUBLIC_DIR, origin)}`;
return `/${relative(vitepressPublicDir, origin)}`;
},
})(schemas);
@@ -458,7 +458,7 @@ async function transformJsonSchema(file: string) {
parser: 'markdown',
...prettierConfig,
});
const newFileName = join(SCHEMA_MARKDOWN_OUTPUT_DIR, `${name}.md`);
const newFileName = join(schemaMarkdownOutputDir, `${name}.md`);
copyTransformedContents(newFileName, !verifyOnly, formatted);
}
}

View File

@@ -24,11 +24,11 @@ describe('docs.mts', () => {
describe('is a code block', () => {
const beforeCodeLine = 'test\n';
const diagram_text = 'graph\n A --> B\n';
const diagramText = 'graph\n A --> B\n';
describe('language = "mermaid-nocode"', () => {
const lang_keyword = 'mermaid-nocode';
const contents = beforeCodeLine + '```' + lang_keyword + '\n' + diagram_text + '\n```\n';
const langKeyword = 'mermaid-nocode';
const contents = beforeCodeLine + '```' + langKeyword + '\n' + diagramText + '\n```\n';
it('changes the language to "mermaid"', async () => {
const result = (
@@ -37,17 +37,16 @@ describe('docs.mts', () => {
.process(contents)
).toString();
expect(result).toEqual(
beforeCodeLine + '\n' + '```' + 'mermaid' + '\n' + diagram_text + '\n```\n'
beforeCodeLine + '\n' + '```' + 'mermaid' + '\n' + diagramText + '\n```\n'
);
});
});
describe('language = "mermaid" | "mmd" | "mermaid-example"', () => {
const mermaid_keywords = ['mermaid', 'mmd', 'mermaid-example'];
const mermaidKeywords = ['mermaid', 'mmd', 'mermaid-example'];
mermaid_keywords.forEach((lang_keyword) => {
const contents =
beforeCodeLine + '```' + lang_keyword + '\n' + diagram_text + '\n```\n';
mermaidKeywords.forEach((langKeyword) => {
const contents = beforeCodeLine + '```' + langKeyword + '\n' + diagramText + '\n```\n';
it('changes the language to "mermaid-example" and adds a copy of the code block with language = "mermaid"', async () => {
const result = (
@@ -59,10 +58,10 @@ describe('docs.mts', () => {
beforeCodeLine +
'\n' +
'```mermaid-example\n' +
diagram_text +
diagramText +
'\n```\n' +
'\n```mermaid\n' +
diagram_text +
diagramText +
'\n```\n'
);
});
@@ -70,9 +69,9 @@ describe('docs.mts', () => {
});
it('calls transformToBlockQuote with the node information', async () => {
const lang_keyword = 'note';
const langKeyword = 'note';
const contents =
beforeCodeLine + '```' + lang_keyword + '\n' + 'This is the text\n' + '```\n';
beforeCodeLine + '```' + langKeyword + '\n' + 'This is the text\n' + '```\n';
const result = (
await remarkBuilder().use(transformMarkdownAst, { originalFilename }).process(contents)

View File

@@ -0,0 +1,8 @@
export const prettierConfig = {
endOfLine: 'auto',
printWidth: 100,
singleQuote: true,
useTabs: false,
tabWidth: 2,
trailingComma: 'es5',
} as const;

View File

@@ -8,32 +8,32 @@ describe('when working with site config', () => {
configApi.setSiteConfig({});
});
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();
const config_2 = configApi.getConfig();
expect(config_1.fontFamily).toEqual(config_0.fontFamily);
expect(config_1.fontSize).toEqual(config_0.fontSize);
expect(config_1).toEqual(config_2);
const config0 = { fontFamily: 'foo-font', fontSize: 150 };
configApi.setSiteConfig(config0);
const config1 = configApi.getSiteConfig();
const config2 = configApi.getConfig();
expect(config1.fontFamily).toEqual(config0.fontFamily);
expect(config1.fontSize).toEqual(config0.fontSize);
expect(config1).toEqual(config2);
});
it('should respect secure keys when applying directives', () => {
const config_0: MermaidConfig = {
const config0: 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);
configApi.setSiteConfig(config0);
const directive: MermaidConfig = {
fontFamily: 'baf',
// fontSize and securityLevel shouldn't be changed
fontSize: 54321,
securityLevel: 'loose',
};
const cfg: MermaidConfig = configApi.updateCurrentConfig(config_0, [directive]);
const cfg: MermaidConfig = configApi.updateCurrentConfig(config0, [directive]);
expect(cfg.fontFamily).toEqual(directive.fontFamily);
expect(cfg.fontSize).toBe(config_0.fontSize);
expect(cfg.securityLevel).toBe(config_0.securityLevel);
expect(cfg.fontSize).toBe(config0.fontSize);
expect(cfg.securityLevel).toBe(config0.securityLevel);
});
it('should allow setting partial options', () => {
const defaultConfig = configApi.getConfig();
@@ -52,30 +52,30 @@ describe('when working with site config', () => {
);
});
it('should set reset config properly', () => {
const config_0 = { fontFamily: 'foo-font', fontSize: 150 };
configApi.setSiteConfig(config_0);
const config_1 = { fontFamily: 'baf' };
configApi.setConfig(config_1);
const config_2 = configApi.getConfig();
expect(config_2.fontFamily).toEqual(config_1.fontFamily);
const config0 = { fontFamily: 'foo-font', fontSize: 150 };
configApi.setSiteConfig(config0);
const config1 = { fontFamily: 'baf' };
configApi.setConfig(config1);
const config2 = configApi.getConfig();
expect(config2.fontFamily).toEqual(config1.fontFamily);
configApi.reset();
const config_3 = configApi.getConfig();
expect(config_3.fontFamily).toEqual(config_0.fontFamily);
const config_4 = configApi.getSiteConfig();
expect(config_4.fontFamily).toEqual(config_0.fontFamily);
const config3 = configApi.getConfig();
expect(config3.fontFamily).toEqual(config0.fontFamily);
const config4 = configApi.getSiteConfig();
expect(config4.fontFamily).toEqual(config0.fontFamily);
});
it('should set global reset config properly', () => {
const config_0 = { fontFamily: 'foo-font', fontSize: 150 };
configApi.setSiteConfig(config_0);
const config_1 = configApi.getSiteConfig();
expect(config_1.fontFamily).toEqual(config_0.fontFamily);
const config_2 = configApi.getConfig();
expect(config_2.fontFamily).toEqual(config_0.fontFamily);
const config0 = { fontFamily: 'foo-font', fontSize: 150 };
configApi.setSiteConfig(config0);
const config1 = configApi.getSiteConfig();
expect(config1.fontFamily).toEqual(config0.fontFamily);
const config2 = configApi.getConfig();
expect(config2.fontFamily).toEqual(config0.fontFamily);
configApi.setConfig({ altFontFamily: 'bar-font' });
const config_3 = configApi.getConfig();
expect(config_3.altFontFamily).toEqual('bar-font');
const config3 = configApi.getConfig();
expect(config3.altFontFamily).toEqual('bar-font');
configApi.reset();
const config_4 = configApi.getConfig();
expect(config_4.altFontFamily).toBeUndefined();
const config4 = configApi.getConfig();
expect(config4.altFontFamily).toBeUndefined();
});
});

View File

@@ -13,13 +13,13 @@ export default intersectPolygon;
* @param point
*/
function intersectPolygon(node, polyPoints, point) {
var x1 = node.x;
var y1 = node.y;
let x1 = node.x;
let y1 = node.y;
var intersections = [];
let intersections = [];
var minX = Number.POSITIVE_INFINITY;
var minY = Number.POSITIVE_INFINITY;
let minX = Number.POSITIVE_INFINITY;
let minY = Number.POSITIVE_INFINITY;
if (typeof polyPoints.forEach === 'function') {
polyPoints.forEach(function (entry) {
minX = Math.min(minX, entry.x);
@@ -30,13 +30,13 @@ function intersectPolygon(node, polyPoints, point) {
minY = Math.min(minY, polyPoints.y);
}
var left = x1 - node.width / 2 - minX;
var top = y1 - node.height / 2 - minY;
let left = x1 - node.width / 2 - minX;
let top = y1 - node.height / 2 - minY;
for (var i = 0; i < polyPoints.length; i++) {
var p1 = polyPoints[i];
var p2 = polyPoints[i < polyPoints.length - 1 ? i + 1 : 0];
var intersect = intersectLine(
for (let i = 0; i < polyPoints.length; i++) {
let p1 = polyPoints[i];
let p2 = polyPoints[i < polyPoints.length - 1 ? i + 1 : 0];
let intersect = intersectLine(
node,
point,
{ x: left + p1.x, y: top + p1.y },
@@ -55,13 +55,13 @@ function intersectPolygon(node, polyPoints, point) {
if (intersections.length > 1) {
// More intersections, find the one nearest to edge end point
intersections.sort(function (p, q) {
var pdx = p.x - point.x;
var pdy = p.y - point.y;
var distp = Math.sqrt(pdx * pdx + pdy * pdy);
let pdx = p.x - point.x;
let pdy = p.y - point.y;
let distp = Math.sqrt(pdx * pdx + pdy * pdy);
var qdx = q.x - point.x;
var qdy = q.y - point.y;
var distq = Math.sqrt(qdx * qdx + qdy * qdy);
let qdx = q.x - point.x;
let qdy = q.y - point.y;
let distq = Math.sqrt(qdx * qdx + qdy * qdy);
return distp < distq ? -1 : distp === distq ? 0 : 1;
});

View File

@@ -119,7 +119,7 @@ const hexagon = async (parent, node) => {
return shapeSvg;
};
const block_arrow = async (parent, node) => {
const blockArrow = async (parent, node) => {
const { shapeSvg, bbox } = await labelHelper(parent, node, undefined, true);
const f = 2;
@@ -140,7 +140,7 @@ const block_arrow = async (parent, node) => {
return shapeSvg;
};
const rect_left_inv_arrow = async (parent, node) => {
const rectLeftInvArrow = async (parent, node) => {
const { shapeSvg, bbox } = await labelHelper(
parent,
node,
@@ -171,7 +171,7 @@ const rect_left_inv_arrow = async (parent, node) => {
return shapeSvg;
};
const lean_right = async (parent, node) => {
const leanRight = async (parent, node) => {
const { shapeSvg, bbox } = await labelHelper(parent, node, getClassesFromNode(node), true);
const w = bbox.width + node.padding;
@@ -194,7 +194,7 @@ const lean_right = async (parent, node) => {
return shapeSvg;
};
const lean_left = async (parent, node) => {
const leanLeft = async (parent, node) => {
const { shapeSvg, bbox } = await labelHelper(
parent,
node,
@@ -250,7 +250,7 @@ const trapezoid = async (parent, node) => {
return shapeSvg;
};
const inv_trapezoid = async (parent, node) => {
const invTrapezoid = async (parent, node) => {
const { shapeSvg, bbox } = await labelHelper(
parent,
node,
@@ -278,7 +278,7 @@ const inv_trapezoid = async (parent, node) => {
return shapeSvg;
};
const rect_right_inv_arrow = async (parent, node) => {
const rectRightInvArrow = async (parent, node) => {
const { shapeSvg, bbox } = await labelHelper(
parent,
node,
@@ -876,7 +876,7 @@ const end = (parent, node) => {
return shapeSvg;
};
const class_box = (parent, node) => {
const classBox = (parent, node) => {
const halfPadding = node.padding / 2;
const rowPadding = 4;
const lineHeight = 8;
@@ -1112,13 +1112,13 @@ const shapes = {
doublecircle,
stadium,
hexagon,
block_arrow,
rect_left_inv_arrow,
lean_right,
lean_left,
block_arrow: blockArrow,
rect_left_inv_arrow: rectLeftInvArrow,
lean_right: leanRight,
lean_left: leanLeft,
trapezoid,
inv_trapezoid,
rect_right_inv_arrow,
inv_trapezoid: invTrapezoid,
rect_right_inv_arrow: rectRightInvArrow,
cylinder,
start,
end,
@@ -1126,7 +1126,7 @@ const shapes = {
subroutine,
fork: forkJoin,
join: forkJoin,
class_box,
class_box: classBox,
};
let nodeElems = {};

View File

@@ -70,8 +70,9 @@ describe('diagram detection', () => {
--------------^
Expecting 'AMP', 'COLON', 'PIPE', 'TESTSTR', 'DOWN', 'DEFAULT', 'NUM', 'COMMA', 'NODE_STRING', 'BRKT', 'MINUS', 'MULT', 'UNICODE_TEXT', got 'EOF']
`);
await expect(Diagram.fromText('sequenceDiagram; A-->B')).rejects
.toThrowErrorMatchingInlineSnapshot(`
await expect(
Diagram.fromText('sequenceDiagram; A-->B')
).rejects.toThrowErrorMatchingInlineSnapshot(`
[Error: Parse error on line 1:
...quenceDiagram; A-->B
-----------------------^

View File

@@ -256,7 +256,7 @@ boundaryStartStatement
boundaryStart
: ENTERPRISE_BOUNDARY attributes {$2.splice(2, 0, 'ENTERPRISE'); yy.addPersonOrSystemBoundary(...$2); $$=$2;}
| SYSTEM_BOUNDARY attributes {$2.splice(2, 0, 'ENTERPRISE'); yy.addPersonOrSystemBoundary(...$2); $$=$2;}
| SYSTEM_BOUNDARY attributes {$2.splice(2, 0, 'SYSTEM'); yy.addPersonOrSystemBoundary(...$2); $$=$2;}
| BOUNDARY attributes {yy.addPersonOrSystemBoundary(...$2); $$=$2;}
| CONTAINER_BOUNDARY attributes {$2.splice(2, 0, 'CONTAINER'); yy.addContainerBoundary(...$2); $$=$2;}
| NODE attributes {yy.addDeploymentNode('node', ...$2); $$=$2;}

View File

@@ -78,28 +78,28 @@ export const drawEdge = function (elem, path, relation, conf, diagObj) {
x = labelPosition.x;
y = labelPosition.y;
let p1_card_x, p1_card_y;
let p2_card_x, p2_card_y;
let p1CardX, p1CardY;
let p2CardX, p2CardY;
if (l % 2 !== 0 && l > 1) {
let cardinality_1_point = utils.calcCardinalityPosition(
let cardinality1Point = utils.calcCardinalityPosition(
relation.relation.type1 !== 'none',
path.points,
path.points[0]
);
let cardinality_2_point = utils.calcCardinalityPosition(
let cardinality2Point = utils.calcCardinalityPosition(
relation.relation.type2 !== 'none',
path.points,
path.points[l - 1]
);
log.debug('cardinality_1_point ' + JSON.stringify(cardinality_1_point));
log.debug('cardinality_2_point ' + JSON.stringify(cardinality_2_point));
log.debug('cardinality_1_point ' + JSON.stringify(cardinality1Point));
log.debug('cardinality_2_point ' + JSON.stringify(cardinality2Point));
p1_card_x = cardinality_1_point.x;
p1_card_y = cardinality_1_point.y;
p2_card_x = cardinality_2_point.x;
p2_card_y = cardinality_2_point.y;
p1CardX = cardinality1Point.x;
p1CardY = cardinality1Point.y;
p2CardX = cardinality2Point.x;
p2CardY = cardinality2Point.y;
}
if (relation.title !== undefined) {
@@ -129,8 +129,8 @@ export const drawEdge = function (elem, path, relation, conf, diagObj) {
const g = elem.append('g').attr('class', 'cardinality');
g.append('text')
.attr('class', 'type1')
.attr('x', p1_card_x)
.attr('y', p1_card_y)
.attr('x', p1CardX)
.attr('y', p1CardY)
.attr('fill', 'black')
.attr('font-size', '6')
.text(relation.relationTitle1);
@@ -139,8 +139,8 @@ export const drawEdge = function (elem, path, relation, conf, diagObj) {
const g = elem.append('g').attr('class', 'cardinality');
g.append('text')
.attr('class', 'type2')
.attr('x', p2_card_x)
.attr('y', p2_card_y)
.attr('x', p2CardX)
.attr('y', p2CardY)
.attr('fill', 'black')
.attr('font-size', '6')
.text(relation.relationTitle2);

View File

@@ -30,18 +30,18 @@ const setupDompurifyHooksIfNotSetup = (() => {
})();
function setupDompurifyHooks() {
const TEMPORARY_ATTRIBUTE = 'data-temp-href-target';
const temporaryAttribute = 'data-temp-href-target';
DOMPurify.addHook('beforeSanitizeAttributes', (node: Element) => {
if (node.tagName === 'A' && node.hasAttribute('target')) {
node.setAttribute(TEMPORARY_ATTRIBUTE, node.getAttribute('target') || '');
node.setAttribute(temporaryAttribute, node.getAttribute('target') || '');
}
});
DOMPurify.addHook('afterSanitizeAttributes', (node: Element) => {
if (node.tagName === 'A' && node.hasAttribute(TEMPORARY_ATTRIBUTE)) {
node.setAttribute('target', node.getAttribute(TEMPORARY_ATTRIBUTE) || '');
node.removeAttribute(TEMPORARY_ATTRIBUTE);
if (node.tagName === 'A' && node.hasAttribute(temporaryAttribute)) {
node.setAttribute('target', node.getAttribute(temporaryAttribute) || '');
node.removeAttribute(temporaryAttribute);
if (node.getAttribute('target') === '_blank') {
node.setAttribute('rel', 'noopener');
}

View File

@@ -53,7 +53,7 @@ function hexagon(parent, bbox, node) {
* @param bbox
* @param node
*/
function rect_left_inv_arrow(parent, bbox, node) {
function rectLeftInvArrow(parent, bbox, node) {
const w = bbox.width;
const h = bbox.height;
const points = [
@@ -75,7 +75,7 @@ function rect_left_inv_arrow(parent, bbox, node) {
* @param bbox
* @param node
*/
function lean_right(parent, bbox, node) {
function leanRight(parent, bbox, node) {
const w = bbox.width;
const h = bbox.height;
const points = [
@@ -96,7 +96,7 @@ function lean_right(parent, bbox, node) {
* @param bbox
* @param node
*/
function lean_left(parent, bbox, node) {
function leanLeft(parent, bbox, node) {
const w = bbox.width;
const h = bbox.height;
const points = [
@@ -138,7 +138,7 @@ function trapezoid(parent, bbox, node) {
* @param bbox
* @param node
*/
function inv_trapezoid(parent, bbox, node) {
function invTrapezoid(parent, bbox, node) {
const w = bbox.width;
const h = bbox.height;
const points = [
@@ -159,7 +159,7 @@ function inv_trapezoid(parent, bbox, node) {
* @param bbox
* @param node
*/
function rect_right_inv_arrow(parent, bbox, node) {
function rectRightInvArrow(parent, bbox, node) {
const w = bbox.width;
const h = bbox.height;
const points = [
@@ -308,22 +308,22 @@ export function addToRender(render) {
render.shapes().cylinder = cylinder;
// Add custom shape for box with inverted arrow on left side
render.shapes().rect_left_inv_arrow = rect_left_inv_arrow;
render.shapes().rect_left_inv_arrow = rectLeftInvArrow;
// Add custom shape for box with inverted arrow on left side
render.shapes().lean_right = lean_right;
render.shapes().lean_right = leanRight;
// Add custom shape for box with inverted arrow on left side
render.shapes().lean_left = lean_left;
render.shapes().lean_left = leanLeft;
// Add custom shape for box with inverted arrow on left side
render.shapes().trapezoid = trapezoid;
// Add custom shape for box with inverted arrow on left side
render.shapes().inv_trapezoid = inv_trapezoid;
render.shapes().inv_trapezoid = invTrapezoid;
// Add custom shape for box with inverted arrow on right side
render.shapes().rect_right_inv_arrow = rect_right_inv_arrow;
render.shapes().rect_right_inv_arrow = rectRightInvArrow;
}
/** @param addShape */
@@ -335,22 +335,22 @@ export function addToRenderV2(addShape) {
addShape({ cylinder });
// Add custom shape for box with inverted arrow on left side
addShape({ rect_left_inv_arrow });
addShape({ rect_left_inv_arrow: rectLeftInvArrow });
// Add custom shape for box with inverted arrow on left side
addShape({ lean_right });
addShape({ lean_right: leanRight });
// Add custom shape for box with inverted arrow on left side
addShape({ lean_left });
addShape({ lean_left: leanLeft });
// Add custom shape for box with inverted arrow on left side
addShape({ trapezoid });
// Add custom shape for box with inverted arrow on left side
addShape({ inv_trapezoid });
addShape({ inv_trapezoid: invTrapezoid });
// Add custom shape for box with inverted arrow on right side
addShape({ rect_right_inv_arrow });
addShape({ rect_right_inv_arrow: rectRightInvArrow });
}
/**

View File

@@ -245,18 +245,20 @@ export const addEdges = async function (edges, g) {
edgeData.arrowTypeStart = 'arrow_open';
edgeData.arrowTypeEnd = 'arrow_open';
/* eslint-disable no-fallthrough */
switch (edge.type) {
// biome-ignore lint/suspicious/noFallthroughSwitchClause: Expected fallthrough
case 'double_arrow_cross':
edgeData.arrowTypeStart = 'arrow_cross';
case 'arrow_cross':
edgeData.arrowTypeEnd = 'arrow_cross';
break;
// biome-ignore lint/suspicious/noFallthroughSwitchClause: Expected fallthrough
case 'double_arrow_point':
edgeData.arrowTypeStart = 'arrow_point';
case 'arrow_point':
edgeData.arrowTypeEnd = 'arrow_point';
break;
// biome-ignore lint/suspicious/noFallthroughSwitchClause: Expected fallthrough
case 'double_arrow_circle':
edgeData.arrowTypeStart = 'arrow_circle';
case 'arrow_circle':
@@ -426,9 +428,9 @@ export const draw = async function (text, id, _version, diagObj) {
selectAll('cluster').append('text');
for (let j = 0; j < subG.nodes.length; j++) {
log.info('Setting up subgraphs', subG.nodes[j], subG.id);
g.setParent(subG.nodes[j], subG.id);
for (const node of subG.nodes) {
log.info('Setting up subgraphs', node, subG.id);
g.setParent(node, subG.id);
}
}
await addVertices(vert, g, id, root, doc, diagObj);

View File

@@ -339,14 +339,14 @@ export const draw = async function (text, id, _version, diagObj) {
selectAll('cluster').append('text');
for (let j = 0; j < subG.nodes.length; j++) {
for (const subGNode of subG.nodes) {
log.warn(
'Setting subgraph',
subG.nodes[j],
diagObj.db.lookUpDomId(subG.nodes[j]),
subGNode,
diagObj.db.lookUpDomId(subGNode),
diagObj.db.lookUpDomId(subG.id)
);
g.setParent(diagObj.db.lookUpDomId(subG.nodes[j]), diagObj.db.lookUpDomId(subG.id));
g.setParent(diagObj.db.lookUpDomId(subGNode), diagObj.db.lookUpDomId(subG.id));
}
}
await addVertices(vert, g, id, root, doc, diagObj);
@@ -429,8 +429,8 @@ export const draw = async function (text, id, _version, diagObj) {
te.attr('transform', `translate(${xPos + _width / 2}, ${yPos + 14})`);
te.attr('id', id + 'Text');
for (let j = 0; j < subG.classes.length; j++) {
clusterEl[0].classList.add(subG.classes[j]);
for (const subGClass of subG.classes) {
clusterEl[0].classList.add(subGClass);
}
}
}

View File

@@ -591,7 +591,7 @@ describe('[Text] when parsing', () => {
it('should throw error for escaping quotes in text state', function () {
//prettier-ignore
const str = 'graph TD; A[This is a \"()\" in text];'; //eslint-disable-line no-useless-escape
const str = 'graph TD; A[This is a "()" in text];'; //eslint-disable-line no-useless-escape
expect(() => flow.parser.parse(str)).toThrowError("got 'STR'");
});

View File

@@ -724,6 +724,7 @@ export const draw = function (text, id, version, diagObj) {
.attr('x', 10)
.attr('y', function (d, i) {
if (i > 0) {
// biome-ignore lint/correctness/noUnreachable: <explanation>
for (let j = 0; j < i; j++) {
prevGap += numOccurrences[i - 1][1];
return (d[1] * theGap) / 2 + prevGap * theGap + theTopPad;

View File

@@ -157,30 +157,40 @@ describe('when parsing a gantt diagram it', function () {
${'crit'} | ${false} | ${false} | ${true} | ${false}
${'active'} | ${false} | ${false} | ${false} | ${true}
${'crit,milestone,done'} | ${true} | ${true} | ${true} | ${false}
`)('should handle a task with tags $tags', ({ tags, milestone, done, crit, active }) => {
const str =
'gantt\n' +
'dateFormat YYYY-MM-DD\n' +
'title Adding gantt diagram functionality to mermaid\n' +
'section Documentation\n' +
'test task:' +
tags +
', 2014-01-01, 2014-01-04';
`)(
'should handle a task with tags $tags',
({
tags,
// Do not remove, these are used in eval.
milestone,
done,
crit,
active,
}) => {
const str =
'gantt\n' +
'dateFormat YYYY-MM-DD\n' +
'title Adding gantt diagram functionality to mermaid\n' +
'section Documentation\n' +
'test task:' +
tags +
', 2014-01-01, 2014-01-04';
const allowedTags = ['active', 'done', 'crit', 'milestone'];
const allowedTags = ['active', 'done', 'crit', 'milestone'];
expect(parserFnConstructor(str)).not.toThrow();
expect(parserFnConstructor(str)).not.toThrow();
const tasks = parser.yy.getTasks();
const tasks = parser.yy.getTasks();
allowedTags.forEach(function (t) {
if (eval(t)) {
expect(tasks[0][t]).toBeTruthy();
} else {
expect(tasks[0][t]).toBeFalsy();
}
});
});
allowedTags.forEach(function (t) {
if (eval(t)) {
expect(tasks[0][t]).toBeTruthy();
} else {
expect(tasks[0][t]).toBeFalsy();
}
});
}
);
it('should parse callback specifier with no args', function () {
spyOn(ganttDb, 'setClickEvent');
const str =
@@ -207,7 +217,6 @@ describe('when parsing a gantt diagram it', function () {
'click cl2 call ganttTestClick("test0", test1, test2)\n';
expect(parserFnConstructor(str)).not.toThrow();
const args = '"test1", "test2", "test3"';
expect(ganttDb.setClickEvent).toHaveBeenCalledWith(
'cl2',
'ganttTestClick',

View File

@@ -147,9 +147,9 @@ export const branch = function (name, order) {
}
};
export const merge = function (otherBranch, custom_id, override_type, custom_tag) {
export const merge = function (otherBranch, customId, overrideType, customTag) {
otherBranch = common.sanitizeText(otherBranch, getConfig());
custom_id = common.sanitizeText(custom_id, getConfig());
customId = common.sanitizeText(customId, getConfig());
const currentCommit = commits.get(branches.get(curBranch));
const otherCommit = commits.get(branches.get(otherBranch));
@@ -209,19 +209,19 @@ export const merge = function (otherBranch, custom_id, override_type, custom_tag
expected: ['branch abc'],
};
throw error;
} else if (custom_id && commits.has(custom_id)) {
} else if (customId && commits.has(customId)) {
let error = new Error(
'Incorrect usage of "merge". Commit with id:' +
custom_id +
customId +
' already exists, use different custom Id'
);
error.hash = {
text: 'merge ' + otherBranch + custom_id + override_type + custom_tag,
token: 'merge ' + otherBranch + custom_id + override_type + custom_tag,
text: 'merge ' + otherBranch + customId + overrideType + customTag,
token: 'merge ' + otherBranch + customId + overrideType + customTag,
line: '1',
loc: { first_line: 1, last_line: 1, first_column: 1, last_column: 1 },
expected: [
'merge ' + otherBranch + ' ' + custom_id + '_UNIQUE ' + override_type + ' ' + custom_tag,
'merge ' + otherBranch + ' ' + customId + '_UNIQUE ' + overrideType + ' ' + customTag,
],
};
@@ -237,15 +237,15 @@ export const merge = function (otherBranch, custom_id, override_type, custom_tag
// } else {
// create merge commit
const commit = {
id: custom_id ? custom_id : seq + '-' + getId(),
id: customId ? customId : seq + '-' + getId(),
message: 'merged branch ' + otherBranch + ' into ' + curBranch,
seq: seq++,
parents: [head == null ? null : head.id, branches.get(otherBranch)],
branch: curBranch,
type: commitType.MERGE,
customType: override_type,
customId: custom_id ? true : false,
tag: custom_tag ? custom_tag : '',
customType: overrideType,
customId: customId ? true : false,
tag: customTag ? customTag : '',
};
head = commit;
commits.set(commit.id, commit);

View File

@@ -405,11 +405,11 @@ const drawCommits = (svg, commits, modifyGraph) => {
text.attr('transform', 'rotate(' + -45 + ', ' + x + ', ' + y + ')');
labelBkg.attr('transform', 'rotate(' + -45 + ', ' + x + ', ' + y + ')');
} else {
let r_x = -7.5 - ((bbox.width + 10) / 25) * 9.5;
let r_y = 10 + (bbox.width / 25) * 8.5;
let rX = -7.5 - ((bbox.width + 10) / 25) * 9.5;
let rY = 10 + (bbox.width / 25) * 8.5;
wrapper.attr(
'transform',
'translate(' + r_x + ', ' + r_y + ') rotate(' + -45 + ', ' + pos + ', ' + y + ')'
'translate(' + rX + ', ' + rY + ') rotate(' + -45 + ', ' + pos + ', ' + y + ')'
);
}
}

View File

@@ -40,9 +40,9 @@ export const draw: DrawDefinition = (text, id, _version, diagObj) => {
const db = diagObj.db as PieDB;
const globalConfig: MermaidConfig = getConfig();
const pieConfig: Required<PieDiagramConfig> = cleanAndMerge(db.getConfig(), globalConfig.pie);
const MARGIN = 40;
const LEGEND_RECT_SIZE = 18;
const LEGEND_SPACING = 4;
const margin = 40;
const legendRectSize = 18;
const legendSpacing = 4;
const height = 450;
const pieWidth: number = height;
const svg: SVG = selectSvgElement(id);
@@ -54,7 +54,7 @@ export const draw: DrawDefinition = (text, id, _version, diagObj) => {
outerStrokeWidth ??= 2;
const textPosition: number = pieConfig.textPosition;
const radius: number = Math.min(pieWidth, height) / 2 - MARGIN;
const radius: number = Math.min(pieWidth, height) / 2 - margin;
// Shape helper to build arcs:
const arcGenerator: d3.Arc<unknown, d3.PieArcDatum<D3Section>> = arc<d3.PieArcDatum<D3Section>>()
.innerRadius(0)
@@ -139,25 +139,25 @@ export const draw: DrawDefinition = (text, id, _version, diagObj) => {
.append('g')
.attr('class', 'legend')
.attr('transform', (_datum, index: number): string => {
const height = LEGEND_RECT_SIZE + LEGEND_SPACING;
const height = legendRectSize + legendSpacing;
const offset = (height * color.domain().length) / 2;
const horizontal = 12 * LEGEND_RECT_SIZE;
const horizontal = 12 * legendRectSize;
const vertical = index * height - offset;
return 'translate(' + horizontal + ',' + vertical + ')';
});
legend
.append('rect')
.attr('width', LEGEND_RECT_SIZE)
.attr('height', LEGEND_RECT_SIZE)
.attr('width', legendRectSize)
.attr('height', legendRectSize)
.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)
.attr('x', legendRectSize + legendSpacing)
.attr('y', legendRectSize - legendSpacing)
.text((datum: d3.PieArcDatum<D3Section>): string => {
const { label, value } = datum.data;
if (db.getShowData()) {
@@ -173,7 +173,7 @@ export const draw: DrawDefinition = (text, id, _version, diagObj) => {
.map((node) => (node as Element)?.getBoundingClientRect().width ?? 0)
);
const totalWidth = pieWidth + MARGIN + LEGEND_RECT_SIZE + LEGEND_SPACING + longestTextWidth;
const totalWidth = pieWidth + margin + legendRectSize + legendSpacing + longestTextWidth;
// Set viewBox
svg.attr('viewBox', `0 0 ${totalWidth} ${height}`);

View File

@@ -45,13 +45,13 @@ class SankeyNode {
constructor(public ID: string) {}
}
const findOrCreateNode = (ID: string): SankeyNode => {
ID = common.sanitizeText(ID, getConfig());
const findOrCreateNode = (id: string): SankeyNode => {
id = common.sanitizeText(id, getConfig());
let node = nodesMap.get(ID);
let node = nodesMap.get(id);
if (node === undefined) {
node = new SankeyNode(ID);
nodesMap.set(ID, node);
node = new SankeyNode(id);
nodesMap.set(id, node);
nodes.push(node);
}
return node;

View File

@@ -33,7 +33,7 @@
"actor" { this.begin('ID'); return 'participant_actor'; }
"create" return 'create';
"destroy" { this.begin('ID'); return 'destroy'; }
<ID>[^\->:\n,;]+?([\-]*[^\->:\n,;]+?)*?(?=((?!\n)\s)+"as"(?!\n)\s|[#\n;]|$) { yytext = yytext.trim(); this.begin('ALIAS'); return 'ACTOR'; }
<ID>[^\<->\->:\n,;]+?([\-]*[^\<->\->:\n,;]+?)*?(?=((?!\n)\s)+"as"(?!\n)\s|[#\n;]|$) { yytext = yytext.trim(); this.begin('ALIAS'); return 'ACTOR'; }
<ALIAS>"as" { this.popState(); this.popState(); this.begin('LINE'); return 'AS'; }
<ALIAS>(?:) { this.popState(); this.popState(); return 'NEWLINE'; }
"loop" { this.begin('LINE'); return 'loop'; }
@@ -73,9 +73,11 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multili
"off" return 'off';
"," return ',';
";" return 'NEWLINE';
[^\+\->:\n,;]+((?!(\-x|\-\-x|\-\)|\-\-\)))[\-]*[^\+\->:\n,;]+)* { yytext = yytext.trim(); return 'ACTOR'; }
[^\+\<->\->:\n,;]+((?!(\-x|\-\-x|\-\)|\-\-\)))[\-]*[^\+\<->\->:\n,;]+)* { yytext = yytext.trim(); return 'ACTOR'; }
"->>" return 'SOLID_ARROW';
"<<->>" return 'BIDIRECTIONAL_SOLID_ARROW';
"-->>" return 'DOTTED_ARROW';
"<<-->>" return 'BIDIRECTIONAL_DOTTED_ARROW';
"->" return 'SOLID_OPEN_ARROW';
"-->" return 'DOTTED_OPEN_ARROW';
\-[x] return 'SOLID_CROSS';
@@ -310,7 +312,9 @@ signaltype
: SOLID_OPEN_ARROW { $$ = yy.LINETYPE.SOLID_OPEN; }
| DOTTED_OPEN_ARROW { $$ = yy.LINETYPE.DOTTED_OPEN; }
| SOLID_ARROW { $$ = yy.LINETYPE.SOLID; }
| BIDIRECTIONAL_SOLID_ARROW { $$ = yy.LINETYPE.BIDIRECTIONAL_SOLID; }
| DOTTED_ARROW { $$ = yy.LINETYPE.DOTTED; }
| BIDIRECTIONAL_DOTTED_ARROW { $$ = yy.LINETYPE.BIDIRECTIONAL_DOTTED; }
| SOLID_CROSS { $$ = yy.LINETYPE.SOLID_CROSS; }
| DOTTED_CROSS { $$ = yy.LINETYPE.DOTTED_CROSS; }
| SOLID_POINT { $$ = yy.LINETYPE.SOLID_POINT; }

View File

@@ -328,6 +328,8 @@ export const LINETYPE = {
BREAK_START: 30,
BREAK_END: 31,
PAR_OVER_START: 32,
BIDIRECTIONAL_SOLID: 33,
BIDIRECTIONAL_DOTTED: 34,
};
export const ARROWTYPE = {

View File

@@ -516,6 +516,36 @@ Alice->>Bob:Hello Bob, how are you?`;
expect(messages.length).toBe(1);
expect(messages[0].type).toBe(diagram.db.LINETYPE.DOTTED);
});
it('should handle bidirectional arrow messages', async () => {
const str = `
sequenceDiagram
Alice<<->>Bob:Hello Bob, how are you?`;
await mermaidAPI.parse(str);
const actors = diagram.db.getActors();
expect(actors.get('Alice').description).toBe('Alice');
expect(actors.get('Bob').description).toBe('Bob');
const messages = diagram.db.getMessages();
expect(messages.length).toBe(1);
expect(messages[0].type).toBe(diagram.db.LINETYPE.BIDIRECTIONAL_SOLID);
});
it('should handle bidirectional dotted arrow messages', async () => {
const str = `
sequenceDiagram
Alice<<-->>Bob:Hello Bob, how are you?`;
await mermaidAPI.parse(str);
const actors = diagram.db.getActors();
expect(actors.get('Alice').description).toBe('Alice');
expect(actors.get('Bob').description).toBe('Bob');
const messages = diagram.db.getMessages();
expect(messages.length).toBe(1);
expect(messages[0].type).toBe(diagram.db.LINETYPE.BIDIRECTIONAL_DOTTED);
});
it('should handle actor activation', async () => {
const str = `
sequenceDiagram

View File

@@ -436,7 +436,8 @@ const drawMessage = async function (diagram, msgModel, lineStartY: number, diagO
type === diagObj.db.LINETYPE.DOTTED ||
type === diagObj.db.LINETYPE.DOTTED_CROSS ||
type === diagObj.db.LINETYPE.DOTTED_POINT ||
type === diagObj.db.LINETYPE.DOTTED_OPEN
type === diagObj.db.LINETYPE.DOTTED_OPEN ||
type === diagObj.db.LINETYPE.BIDIRECTIONAL_DOTTED
) {
line.style('stroke-dasharray', '3, 3');
line.attr('class', 'messageLine1');
@@ -462,6 +463,13 @@ const drawMessage = async function (diagram, msgModel, lineStartY: number, diagO
if (type === diagObj.db.LINETYPE.SOLID || type === diagObj.db.LINETYPE.DOTTED) {
line.attr('marker-end', 'url(' + url + '#arrowhead)');
}
if (
type === diagObj.db.LINETYPE.BIDIRECTIONAL_SOLID ||
type === diagObj.db.LINETYPE.BIDIRECTIONAL_DOTTED
) {
line.attr('marker-start', 'url(' + url + '#arrowhead)');
line.attr('marker-end', 'url(' + url + '#arrowhead)');
}
if (type === diagObj.db.LINETYPE.SOLID_POINT || type === diagObj.db.LINETYPE.DOTTED_POINT) {
line.attr('marker-end', 'url(' + url + '#filled-head)');
}
@@ -1036,6 +1044,8 @@ export const draw = async function (_text: string, id: string, _version: string,
diagObj.db.LINETYPE.DOTTED_CROSS,
diagObj.db.LINETYPE.SOLID_POINT,
diagObj.db.LINETYPE.DOTTED_POINT,
diagObj.db.LINETYPE.BIDIRECTIONAL_SOLID,
diagObj.db.LINETYPE.BIDIRECTIONAL_DOTTED,
].includes(msg.type)
) {
sequenceIndex = sequenceIndex + sequenceIndexStep;
@@ -1416,6 +1426,8 @@ const buildMessageModel = function (msg, actors, diagObj) {
diagObj.db.LINETYPE.DOTTED_CROSS,
diagObj.db.LINETYPE.SOLID_POINT,
diagObj.db.LINETYPE.DOTTED_POINT,
diagObj.db.LINETYPE.BIDIRECTIONAL_SOLID,
diagObj.db.LINETYPE.BIDIRECTIONAL_DOTTED,
].includes(msg.type)
) {
return {};
@@ -1423,7 +1435,7 @@ const buildMessageModel = function (msg, actors, diagObj) {
const [fromLeft, fromRight] = activationBounds(msg.from, actors);
const [toLeft, toRight] = activationBounds(msg.to, actors);
const isArrowToRight = fromLeft <= toLeft;
const startx = isArrowToRight ? fromRight : fromLeft;
let 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.
@@ -1462,6 +1474,17 @@ const buildMessageModel = function (msg, actors, diagObj) {
if (![diagObj.db.LINETYPE.SOLID_OPEN, diagObj.db.LINETYPE.DOTTED_OPEN].includes(msg.type)) {
stopx += adjustValue(3);
}
/**
* Shorten start position of bidirectional arrow to accommodate for second arrowhead
*/
if (
[diagObj.db.LINETYPE.BIDIRECTIONAL_SOLID, diagObj.db.LINETYPE.BIDIRECTIONAL_DOTTED].includes(
msg.type
)
) {
startx -= adjustValue(3);
}
}
const allBounds = [fromLeft, fromRight, toLeft, toRight];

View File

@@ -49,11 +49,11 @@ export const drawPopup = function (elem, actor, minMenuWidth, textAttrs, forceMe
rectElem.attr('height', rectData.height);
rectElem.attr('rx', rectData.rx);
rectElem.attr('ry', rectData.ry);
let linkY = 20;
if (links != null) {
var linkY = 20;
for (let key in links) {
var linkElem = g.append('a');
var sanitizedLink = sanitizeUrl(links[key]);
const linkElem = g.append('a');
const sanitizedLink = sanitizeUrl(links[key]);
linkElem.attr('xlink:href', sanitizedLink);
linkElem.attr('target', '_blank');
@@ -415,11 +415,11 @@ const drawActorTypeActor = async function (elem, actor, conf, isFooter) {
const center = actor.x + actor.width / 2;
const centerY = actorY + 80;
elem.lower();
const line = elem.append('g').lower();
if (!isFooter) {
actorCnt++;
elem
line
.append('line')
.attr('id', 'actor' + actorCnt)
.attr('x1', center)
@@ -735,7 +735,7 @@ export const insertArrowHead = function (elem) {
.attr('markerUnits', 'userSpaceOnUse')
.attr('markerWidth', 12)
.attr('markerHeight', 12)
.attr('orient', 'auto')
.attr('orient', 'auto-start-reverse')
.append('path')
.attr('d', 'M -1 0 L 10 5 L 0 10 z'); // this is actual shape for arrowhead
};

View File

@@ -88,9 +88,9 @@ describe('ClassDefs and classes when parsing a State diagram', () => {
expect(classes.get('exampleStyleClass').styles[0]).toEqual('background:#bbb');
expect(classes.get('exampleStyleClass').styles[1]).toEqual('border:1px solid red');
const state_a = stateDb.getState('a');
expect(state_a.classes.length).toEqual(1);
expect(state_a.classes[0]).toEqual('exampleStyleClass');
const stateA = stateDb.getState('a');
expect(stateA.classes.length).toEqual(1);
expect(stateA.classes[0]).toEqual('exampleStyleClass');
});
it('can be applied to a state with an id containing _', function () {
@@ -109,9 +109,9 @@ describe('ClassDefs and classes when parsing a State diagram', () => {
expect(classes.get('exampleStyleClass').styles[0]).toBe('background:#bbb');
expect(classes.get('exampleStyleClass').styles[1]).toBe('border:1px solid red');
const state_a_a = stateDiagram.parser.yy.getState('a_a');
expect(state_a_a.classes.length).toEqual(1);
expect(state_a_a.classes[0]).toEqual('exampleStyleClass');
const stateAA = stateDiagram.parser.yy.getState('a_a');
expect(stateAA.classes.length).toEqual(1);
expect(stateAA.classes[0]).toEqual('exampleStyleClass');
});
describe('::: syntax', () => {

View File

@@ -130,12 +130,12 @@ describe('state diagram V2, ', function () {
stateDiagram.parser.yy.extract(stateDiagram.parser.yy.getRootDocV2());
const rels = stateDb.getRelations();
const rel_1_2 = rels.find((rel) => rel.id1 === 'State1' && rel.id2 === 'State2');
expect(rel_1_2.relationTitle).toEqual('Transition 1');
const rel_1_3 = rels.find((rel) => rel.id1 === 'State1' && rel.id2 === 'State3');
expect(rel_1_3.relationTitle).toEqual('Transition 2');
const rel_1_4 = rels.find((rel) => rel.id1 === 'State1' && rel.id2 === 'State4');
expect(rel_1_4.relationTitle).toEqual('Transition 3');
const rel12 = rels.find((rel) => rel.id1 === 'State1' && rel.id2 === 'State2');
expect(rel12.relationTitle).toEqual('Transition 1');
const rel13 = rels.find((rel) => rel.id1 === 'State1' && rel.id2 === 'State3');
expect(rel13.relationTitle).toEqual('Transition 2');
const rel14 = rels.find((rel) => rel.id1 === 'State1' && rel.id2 === 'State4');
expect(rel14.relationTitle).toEqual('Transition 3');
});
});
@@ -408,10 +408,10 @@ describe('state diagram V2, ', function () {
expect(states.get('Active').doc[0].id).toEqual('Idle');
const rels = stateDb.getRelations();
const rel_Inactive_Idle = rels.find((rel) => rel.id1 === 'Inactive' && rel.id2 === 'Idle');
expect(rel_Inactive_Idle.relationTitle).toEqual('ACT');
const rel_Active_Active = rels.find((rel) => rel.id1 === 'Active' && rel.id2 === 'Active');
expect(rel_Active_Active.relationTitle).toEqual('LOG');
const relInactiveIdle = rels.find((rel) => rel.id1 === 'Inactive' && rel.id2 === 'Idle');
expect(relInactiveIdle.relationTitle).toEqual('ACT');
const relActiveActive = rels.find((rel) => rel.id1 === 'Active' && rel.id2 === 'Active');
expect(relActiveActive.relationTitle).toEqual('LOG');
});
});
});

View File

@@ -362,8 +362,7 @@ const setupDoc = (g, parentParsedItem, doc, diagramStates, diagramDb, altFlag) =
const getDir = (parsedItem, defaultDir = DEFAULT_NESTED_DOC_DIR) => {
let dir = defaultDir;
if (parsedItem.doc) {
for (let i = 0; i < parsedItem.doc.length; i++) {
const parsedItemDoc = parsedItem.doc[i];
for (const parsedItemDoc of parsedItem.doc) {
if (parsedItemDoc.stmt === 'dir') {
dir = parsedItemDoc.value;
}

View File

@@ -15,9 +15,7 @@ const genSections = (options) => {
for (let i = 0; i < options.THEME_COLOR_LIMIT; i++) {
const sw = '' + (17 - 3 * i);
sections += `
.section-${i - 1} rect, .section-${i - 1} path, .section-${i - 1} circle, .section-${
i - 1
} path {
.section-${i - 1} rect, .section-${i - 1} path, .section-${i - 1} circle, .section-${i - 1} path {
fill: ${options['cScale' + i]};
}
.section-${i - 1} text {

View File

@@ -29,7 +29,7 @@ export const draw = function (text: string, id: string, version: string, diagObj
//1. Fetch the configuration
const conf = getConfig();
// @ts-expect-error - wrong config?
const LEFT_MARGIN = conf.leftMargin ?? 50;
const leftMargin = conf.leftMargin ?? 50;
log.debug('timeline', diagObj.db);
@@ -68,7 +68,7 @@ export const draw = function (text: string, id: string, version: string, diagObj
//let sectionBeginX = 0;
let depthY = 0;
let sectionBeginY = 0;
let masterX = 50 + LEFT_MARGIN;
let masterX = 50 + leftMargin;
//sectionBeginX = masterX;
let masterY = 50;
sectionBeginY = 50;
@@ -115,8 +115,7 @@ export const draw = function (text: string, id: string, version: string, diagObj
maxEventCount = Math.max(maxEventCount, task.events.length);
//calculate maxEventLineLength
let maxEventLineLengthTemp = 0;
for (let j = 0; j < task.events.length; j++) {
const event = task.events[j];
for (const event of task.events) {
const eventNode = {
descr: event,
section: task.section,
@@ -203,7 +202,7 @@ export const draw = function (text: string, id: string, version: string, diagObj
svg
.append('text')
.text(title)
.attr('x', box.width / 2 - LEFT_MARGIN)
.attr('x', box.width / 2 - leftMargin)
.attr('font-size', '4ex')
.attr('font-weight', 'bold')
.attr('y', 20);
@@ -215,9 +214,9 @@ export const draw = function (text: string, id: string, version: string, diagObj
// Draw activity line
lineWrapper
.append('line')
.attr('x1', LEFT_MARGIN)
.attr('x1', leftMargin)
.attr('y1', depthY) // One section head + one task + margins
.attr('x2', box.width + 3 * LEFT_MARGIN) // Subtract stroke width so arrow point is retained
.attr('x2', box.width + 3 * leftMargin) // Subtract stroke width so arrow point is retained
.attr('y2', depthY)
.attr('stroke-width', 4)
.attr('stroke', 'black')

View File

@@ -75,8 +75,8 @@ function textSanitizer(text: string) {
return sanitizeText(text.trim(), config);
}
function setTmpSVGG(SVGG: Group) {
tmpSVGGroup = SVGG;
function setTmpSVGG(svgg: Group) {
tmpSVGGroup = svgg;
}
function setOrientation(orientation: string) {
if (orientation === 'horizontal') {
@@ -143,7 +143,7 @@ function transformDataWithoutCategory(data: number[]): SimplePlotDataType {
if (isLinearAxisData(xyChartData.xAxis)) {
const min = xyChartData.xAxis.min;
const max = xyChartData.xAxis.max;
const step = (max - min + 1) / data.length;
const step = (max - min) / (data.length - 1);
const categories: string[] = [];
for (let i = min; i <= max; i += step) {
categories.push(`${i}`);

View File

@@ -1,4 +1,4 @@
import { defineConfig, MarkdownOptions } from 'vitepress';
import { defineConfig, type MarkdownOptions } from 'vitepress';
import { version } from '../../../package.json';
import MermaidExample from './mermaid-markdown-all.js';

View File

@@ -1,5 +1,5 @@
import contributorUsernamesJson from './contributor-names.json';
import { CoreTeam, knut, plainTeamMembers } from './teamMembers.js';
import { type CoreTeam, knut, plainTeamMembers } from './teamMembers.js';
const contributorUsernames: string[] = contributorUsernamesJson;

View File

@@ -32,7 +32,7 @@ async function fetchAvatars() {
});
contributors = JSON.parse(await readFile(pathContributors, { encoding: 'utf-8' }));
let avatars = contributors.map((name) => {
const avatars = contributors.map((name) => {
download(`https://github.com/${name}.png?size=100`, getAvatarPath(name));
});

View File

@@ -61,7 +61,7 @@ onMounted(async () => {
//refresh images on first render
const hasImages = /<img([\w\W]+?)>/.exec(code.value)?.length > 0;
if (hasImages)
if (hasImages) {
setTimeout(() => {
let imgElements = document.getElementsByTagName('img');
let imgs = Array.from(imgElements);
@@ -80,6 +80,7 @@ onMounted(async () => {
});
}
}, 100);
}
});
onUnmounted(() => mut.disconnect());

View File

@@ -52,7 +52,7 @@ The following commands must be sufficient enough to start with:
```bash
curl -fsSL https://get.pnpm.io/install.sh | sh -
pnpm env use --global 18
pnpm env use --global 20
```
You may also need to reload `.shrc` or `.bashrc` afterwards.

View File

@@ -141,18 +141,20 @@ Messages can be of two displayed either solid or with a dotted line.
[Actor][Arrow][Actor]:Message text
```
There are six types of arrows currently supported:
There are ten types of arrows currently supported:
| Type | Description |
| ------ | ------------------------------------------------ |
| `->` | Solid line without arrow |
| `-->` | Dotted line without arrow |
| `->>` | Solid line with arrowhead |
| `-->>` | Dotted line with arrowhead |
| `-x` | Solid line with a cross at the end |
| `--x` | Dotted line with a cross at the end. |
| `-)` | Solid line with an open arrow at the end (async) |
| `--)` | Dotted line with a open arrow at the end (async) |
| Type | Description |
| -------- | ----------------------------------------------------------------------- |
| `->` | Solid line without arrow |
| `-->` | Dotted line without arrow |
| `->>` | Solid line with arrowhead |
| `-->>` | Dotted line with arrowhead |
| `<<->>` | Solid line with bidirectional arrowheads (v<MERMAID_RELEASE_VERSION>+) |
| `<<-->>` | Dotted line with bidirectional arrowheads (v<MERMAID_RELEASE_VERSION>+) |
| `-x` | Solid line with a cross at the end |
| `--x` | Dotted line with a cross at the end. |
| `-)` | Solid line with an open arrow at the end (async) |
| `--)` | Dotted line with a open arrow at the end (async) |
## Activations
@@ -205,11 +207,22 @@ sequenceDiagram
Note over Alice,John: A typical interaction
```
It is also possible to add a line break (applies to text input in general):
## Line breaks
Line break can be added to Note and Message:
```mermaid-example
sequenceDiagram
Alice->John: Hello John, how are you?
Alice->John: Hello John,<br/>how are you?
Note over Alice,John: A typical interaction<br/>But now in two lines
```
Line breaks in Actor names requires aliases:
```mermaid-example
sequenceDiagram
participant Alice as Alice<br/>Johnson
Alice->John: Hello John,<br/>how are you?
Note over Alice,John: A typical interaction<br/>But now in two lines
```

View File

@@ -170,8 +170,9 @@ describe('when using mermaid and ', () => {
).resolves.not.toThrow();
});
it('should throw for an invalid flow definition', async () => {
await expect(mermaid.parse('graph TQ;A--x|text including URL space|B;')).rejects
.toThrowErrorMatchingInlineSnapshot(`
await expect(
mermaid.parse('graph TQ;A--x|text including URL space|B;')
).rejects.toThrowErrorMatchingInlineSnapshot(`
[Error: Lexical error on line 1. Unrecognized text.
graph TQ;A--x|text includ
-----^]
@@ -207,7 +208,7 @@ describe('when using mermaid and ', () => {
[Error: Parse error on line 2:
...equenceDiagramAlice:->Bob: Hello Bob, h...
----------------------^
Expecting 'SOLID_OPEN_ARROW', 'DOTTED_OPEN_ARROW', 'SOLID_ARROW', 'DOTTED_ARROW', 'SOLID_CROSS', 'DOTTED_CROSS', 'SOLID_POINT', 'DOTTED_POINT', got 'TXT']
Expecting 'SOLID_OPEN_ARROW', 'DOTTED_OPEN_ARROW', 'SOLID_ARROW', 'BIDIRECTIONAL_SOLID_ARROW', 'DOTTED_ARROW', 'BIDIRECTIONAL_DOTTED_ARROW', 'SOLID_CROSS', 'DOTTED_CROSS', 'SOLID_POINT', 'DOTTED_POINT', got 'TXT']
`);
});

View File

@@ -69,6 +69,7 @@ vi.mock('stylis', () => {
import { compile, serialize } from 'stylis';
import { decodeEntities, encodeEntities } from './utils.js';
import { Diagram } from './Diagram.js';
import { toBase64 } from './utils/base64.js';
/**
* @see https://vitest.dev/guide/mocking.html Mock part of a module
@@ -176,7 +177,7 @@ describe('mermaidAPI', () => {
});
describe('putIntoIFrame', () => {
const inputSvgCode = 'this is the SVG code';
const inputSvgCode = 'this is the SVG code';
it('uses the default SVG iFrame height is used if no svgElement given', () => {
const result = putIntoIFrame(inputSvgCode);
@@ -199,15 +200,14 @@ describe('mermaidAPI', () => {
});
it('sets src to base64 version of <body style="IFRAME_SVG_BODY_STYLE">svgCode<//body>', () => {
const base64encodedSrc = btoa('<body style="' + 'margin:0' + '">' + inputSvgCode + '</body>');
const expectedRegExp = new RegExp('src="data:text/html;base64,' + base64encodedSrc + '"');
const base64encodedSrc = toBase64(`<body style="margin:0">${inputSvgCode}</body>`);
const expectedSrc = `src="data:text/html;charset=UTF-8;base64,${base64encodedSrc}"`;
const result = putIntoIFrame(inputSvgCode);
expect(result).toMatch(expectedRegExp);
expect(result).toContain(expectedSrc);
});
it('uses the height and appends px from the svgElement given', () => {
const faux_svgElement = {
const fauxSvgElement = {
viewBox: {
baseVal: {
height: 42,
@@ -215,7 +215,7 @@ describe('mermaidAPI', () => {
},
};
const result = putIntoIFrame(inputSvgCode, faux_svgElement);
const result = putIntoIFrame(inputSvgCode, fauxSvgElement);
expect(result).toMatch(/style="(.*)height:42px;/);
});
});
@@ -226,54 +226,54 @@ describe('mermaidAPI', () => {
describe('appendDivSvgG', () => {
const fauxGNode = new MockedD3();
const parent_append_spy = vi.spyOn(fauxParentNode, 'append').mockReturnValue(fauxEnclosingDiv);
const div_append_spy = vi.spyOn(fauxEnclosingDiv, 'append').mockReturnValue(fauxSvgNode);
const parentAppendSpy = vi.spyOn(fauxParentNode, 'append').mockReturnValue(fauxEnclosingDiv);
const divAppendSpy = vi.spyOn(fauxEnclosingDiv, 'append').mockReturnValue(fauxSvgNode);
// @ts-ignore @todo TODO why is this getting a type error?
const div_attr_spy = vi.spyOn(fauxEnclosingDiv, 'attr').mockReturnValue(fauxEnclosingDiv);
const svg_append_spy = vi.spyOn(fauxSvgNode, 'append').mockReturnValue(fauxGNode);
const divAttrSpy = vi.spyOn(fauxEnclosingDiv, 'attr').mockReturnValue(fauxEnclosingDiv);
const svgAppendSpy = vi.spyOn(fauxSvgNode, 'append').mockReturnValue(fauxGNode);
// @ts-ignore @todo TODO why is this getting a type error?
const svg_attr_spy = vi.spyOn(fauxSvgNode, 'attr').mockReturnValue(fauxSvgNode);
const svgAttrSpy = vi.spyOn(fauxSvgNode, 'attr').mockReturnValue(fauxSvgNode);
// cspell:ignore dthe
it('appends a div node', () => {
appendDivSvgG(fauxParentNode, 'theId', 'dtheId');
expect(parent_append_spy).toHaveBeenCalledWith('div');
expect(div_append_spy).toHaveBeenCalledWith('svg');
expect(parentAppendSpy).toHaveBeenCalledWith('div');
expect(divAppendSpy).toHaveBeenCalledWith('svg');
});
it('the id for the div is "d" with the id appended', () => {
appendDivSvgG(fauxParentNode, 'theId', 'dtheId');
expect(div_attr_spy).toHaveBeenCalledWith('id', 'dtheId');
expect(divAttrSpy).toHaveBeenCalledWith('id', 'dtheId');
});
it('sets the style for the div if one is given', () => {
appendDivSvgG(fauxParentNode, 'theId', 'dtheId', 'given div style', 'given x link');
expect(div_attr_spy).toHaveBeenCalledWith('style', 'given div style');
expect(divAttrSpy).toHaveBeenCalledWith('style', 'given div style');
});
it('appends a svg node to the div node', () => {
appendDivSvgG(fauxParentNode, 'theId', 'dtheId');
expect(div_attr_spy).toHaveBeenCalledWith('id', 'dtheId');
expect(divAttrSpy).toHaveBeenCalledWith('id', 'dtheId');
});
it('sets the svg width to 100%', () => {
appendDivSvgG(fauxParentNode, 'theId', 'dtheId');
expect(svg_attr_spy).toHaveBeenCalledWith('width', '100%');
expect(svgAttrSpy).toHaveBeenCalledWith('width', '100%');
});
it('the svg id is the id', () => {
appendDivSvgG(fauxParentNode, 'theId', 'dtheId');
expect(svg_attr_spy).toHaveBeenCalledWith('id', 'theId');
expect(svgAttrSpy).toHaveBeenCalledWith('id', 'theId');
});
it('the svg xml namespace is the 2000 standard', () => {
appendDivSvgG(fauxParentNode, 'theId', 'dtheId');
expect(svg_attr_spy).toHaveBeenCalledWith('xmlns', 'http://www.w3.org/2000/svg');
expect(svgAttrSpy).toHaveBeenCalledWith('xmlns', 'http://www.w3.org/2000/svg');
});
it('sets the svg xlink if one is given', () => {
appendDivSvgG(fauxParentNode, 'theId', 'dtheId', 'div style', 'given x link');
expect(svg_attr_spy).toHaveBeenCalledWith('xmlns:xlink', 'given x link');
expect(svgAttrSpy).toHaveBeenCalledWith('xmlns:xlink', 'given x link');
});
it('appends a g (group) node to the svg node', () => {
appendDivSvgG(fauxParentNode, 'theId', 'dtheId');
expect(svg_append_spy).toHaveBeenCalledWith('g');
expect(svgAppendSpy).toHaveBeenCalledWith('g');
});
it('returns the given parentRoot d3 nodes', () => {
expect(appendDivSvgG(fauxParentNode, 'theId', 'dtheId')).toEqual(fauxParentNode);
@@ -283,7 +283,7 @@ describe('mermaidAPI', () => {
describe('createCssStyles', () => {
const serif = 'serif';
const sansSerif = 'sans-serif';
const mocked_config_with_htmlLabels: MermaidConfig = {
const mockedConfigWithHtmlLabels: MermaidConfig = {
themeCSS: 'default',
fontFamily: serif,
altFontFamily: sansSerif,
@@ -291,15 +291,15 @@ describe('mermaidAPI', () => {
};
it('gets the cssStyles from the theme', () => {
const styles = createCssStyles(mocked_config_with_htmlLabels, null);
const styles = createCssStyles(mockedConfigWithHtmlLabels, null);
expect(styles).toMatch(/^\ndefault(.*)/);
});
it('gets the fontFamily from the config', () => {
const styles = createCssStyles(mocked_config_with_htmlLabels, new Map());
const styles = createCssStyles(mockedConfigWithHtmlLabels, new Map());
expect(styles).toMatch(/(.*)\n:root { --mermaid-font-family: serif(.*)/);
});
it('gets the alt fontFamily from the config', () => {
const styles = createCssStyles(mocked_config_with_htmlLabels, undefined);
const styles = createCssStyles(mockedConfigWithHtmlLabels, undefined);
expect(styles).toMatch(/(.*)\n:root { --mermaid-alt-font-family: sans-serif(.*)/);
});
@@ -310,13 +310,13 @@ describe('mermaidAPI', () => {
const classDefs = { classDef1, classDef2, classDef3 };
describe('the graph supports classDefs', () => {
const REGEXP_SPECIALS = ['^', '$', '?', '(', '{', '[', '.', '*', '!'];
const regexpSpecials = ['^', '$', '?', '(', '{', '[', '.', '*', '!'];
// prefix any special RegExp characters in the given string with a \ so we can use the literal character in a RegExp
function escapeForRegexp(str: string) {
const strChars = [...str]; // split into array of every char
const strEscaped = strChars.map((char) => {
if (REGEXP_SPECIALS.includes(char)) {
if (regexpSpecials.includes(char)) {
return `\\${char}`;
} else {
return char;
@@ -326,7 +326,7 @@ describe('mermaidAPI', () => {
}
// Common test expecting given styles to have .classDef1 and .classDef2 statements but not .classDef3
function expect_styles_matchesHtmlElements(styles: string, htmlElement: string) {
function expectStylesMatchesHtmlElements(styles: string, htmlElement: string) {
expect(styles).toMatch(
new RegExp(
`\\.classDef1 ${escapeForRegexp(
@@ -344,7 +344,7 @@ describe('mermaidAPI', () => {
}
// Common test expecting given textStyles to have .classDef2 and .classDef3 statements but not .classDef1
function expect_textStyles_matchesHtmlElements(textStyles: string, htmlElement: string) {
function expectTextStylesMatchesHtmlElements(textStyles: string, htmlElement: string) {
expect(textStyles).toMatch(
new RegExp(
`\\.classDef2 ${escapeForRegexp(htmlElement)} \\{ textStyle2-1 !important; }`
@@ -367,7 +367,7 @@ describe('mermaidAPI', () => {
}
// common suite and tests to verify that the right styles are created with the right htmlElements
function expect_correct_styles_with_htmlElements(mocked_config: MermaidConfig) {
function expectCorrectStylesWithHtmlElements(mockedConfig: MermaidConfig) {
describe('creates styles for "> *" and "span" elements', () => {
const htmlElements = ['> *', 'span'];
@@ -375,21 +375,21 @@ describe('mermaidAPI', () => {
// @todo TODO Can't figure out how to spy on the cssImportantStyles method.
// That would be a much better approach than manually checking the result
const styles = createCssStyles(mocked_config, new Map(Object.entries(classDefs)));
const styles = createCssStyles(mockedConfig, new Map(Object.entries(classDefs)));
htmlElements.forEach((htmlElement) => {
expect_styles_matchesHtmlElements(styles, htmlElement);
expectStylesMatchesHtmlElements(styles, htmlElement);
});
expect_textStyles_matchesHtmlElements(styles, 'tspan');
expectTextStylesMatchesHtmlElements(styles, 'tspan');
});
});
}
it('there are htmlLabels in the configuration', () => {
expect_correct_styles_with_htmlElements(mocked_config_with_htmlLabels);
expectCorrectStylesWithHtmlElements(mockedConfigWithHtmlLabels);
});
it('there are flowchart.htmlLabels in the configuration', () => {
const mocked_config_flowchart_htmlLabels: MermaidConfig = {
const mockedConfigFlowchartHtmlLabels: MermaidConfig = {
themeCSS: 'default',
fontFamily: 'serif',
altFontFamily: 'sans-serif',
@@ -397,11 +397,11 @@ describe('mermaidAPI', () => {
htmlLabels: true,
},
};
expect_correct_styles_with_htmlElements(mocked_config_flowchart_htmlLabels);
expectCorrectStylesWithHtmlElements(mockedConfigFlowchartHtmlLabels);
});
describe('no htmlLabels in the configuration', () => {
const mocked_config_no_htmlLabels = {
const mockedConfigNoHtmlLabels = {
themeCSS: 'default',
fontFamily: 'serif',
altFontFamily: 'sans-serif',
@@ -414,13 +414,13 @@ describe('mermaidAPI', () => {
// TODO Can't figure out how to spy on the cssImportantStyles method. That would be a much better approach than manually checking the result.
const styles = createCssStyles(
mocked_config_no_htmlLabels,
mockedConfigNoHtmlLabels,
new Map(Object.entries(classDefs))
);
htmlElements.forEach((htmlElement) => {
expect_styles_matchesHtmlElements(styles, htmlElement);
expectStylesMatchesHtmlElements(styles, htmlElement);
});
expect_textStyles_matchesHtmlElements(styles, 'tspan');
expectTextStylesMatchesHtmlElements(styles, 'tspan');
});
});
});
@@ -692,8 +692,9 @@ describe('mermaidAPI', () => {
).resolves.toBe(false);
});
it('resolves for valid definition', async () => {
await expect(mermaidAPI.parse('graph TD;A--x|text including URL space|B;')).resolves
.toMatchInlineSnapshot(`
await expect(
mermaidAPI.parse('graph TD;A--x|text including URL space|B;')
).resolves.toMatchInlineSnapshot(`
{
"diagramType": "flowchart-v2",
}
@@ -748,15 +749,12 @@ describe('mermaidAPI', () => {
const expectedDiagramType = testedDiagram.expectedType;
it('should set aria-roledescription to the diagram type AND should call addSVGa11yTitleDescription', async () => {
const a11yDiagramInfo_spy = vi.spyOn(accessibility, 'setA11yDiagramInfo');
const a11yTitleDesc_spy = vi.spyOn(accessibility, 'addSVGa11yTitleDescription');
const a11yDiagramInfoSpy = vi.spyOn(accessibility, 'setA11yDiagramInfo');
const a11yTitleDescSpy = vi.spyOn(accessibility, 'addSVGa11yTitleDescription');
const result = await mermaidAPI.render(id, diagramText);
expect(result.diagramType).toBe(expectedDiagramType);
expect(a11yDiagramInfo_spy).toHaveBeenCalledWith(
expect.anything(),
expectedDiagramType
);
expect(a11yTitleDesc_spy).toHaveBeenCalled();
expect(a11yDiagramInfoSpy).toHaveBeenCalledWith(expect.anything(), expectedDiagramType);
expect(a11yTitleDescSpy).toHaveBeenCalled();
});
});
});

View File

@@ -31,6 +31,7 @@ import { setA11yDiagramInfo, addSVGa11yTitleDescription } from './accessibility.
import type { DiagramMetadata, DiagramStyleClassDef } from './diagram-api/types.js';
import { preprocessDiagram } from './preprocess.js';
import { decodeEntities } from './utils.js';
import { toBase64 } from './utils/base64.js';
const MAX_TEXTLENGTH = 50_000;
const MAX_TEXTLENGTH_EXCEEDED_MSG =
@@ -248,14 +249,13 @@ export const cleanUpSvgCode = (
* @param svgCode - the svg code to put inside the iFrame
* @param svgElement - the d3 node that has the current svgElement so we can get the height from it
* @returns - the code with the iFrame that now contains the svgCode
* TODO replace btoa(). Replace with buf.toString('base64')?
*/
export const putIntoIFrame = (svgCode = '', svgElement?: D3Element): string => {
const height = svgElement?.viewBox?.baseVal?.height
? svgElement.viewBox.baseVal.height + 'px'
: IFRAME_HEIGHT;
const base64encodedSrc = btoa('<body style="' + IFRAME_BODY_STYLE + '">' + svgCode + '</body>');
return `<iframe style="width:${IFRAME_WIDTH};height:${height};${IFRAME_STYLES}" src="data:text/html;base64,${base64encodedSrc}" sandbox="${IFRAME_SANDBOX_OPTS}">
const base64encodedSrc = toBase64(`<body style="${IFRAME_BODY_STYLE}">${svgCode}</body>`);
return `<iframe style="width:${IFRAME_WIDTH};height:${height};${IFRAME_STYLES}" src="data:text/html;charset=UTF-8;base64,${base64encodedSrc}" sandbox="${IFRAME_SANDBOX_OPTS}">
${IFRAME_NOT_SUPPORTED_MSG}
</iframe>`;
};
@@ -363,14 +363,14 @@ const render = async function (
const idSelector = '#' + id;
const iFrameID = 'i' + id;
const iFrameID_selector = '#' + iFrameID;
const iFrameIdSelector = '#' + iFrameID;
const enclosingDivID = 'd' + id;
const enclosingDivID_selector = '#' + enclosingDivID;
const enclosingDivIdSelector = '#' + enclosingDivID;
const removeTempElements = () => {
// -------------------------------------------------------------------------------
// Remove the temporary HTML element if appropriate
const tmpElementSelector = isSandboxed ? iFrameID_selector : enclosingDivID_selector;
const tmpElementSelector = isSandboxed ? iFrameIdSelector : enclosingDivIdSelector;
const node = select(tmpElementSelector).node();
if (node && 'remove' in node) {
node.remove();
@@ -442,7 +442,7 @@ const render = async function (
}
// Get the temporary div element containing the svg
const element = root.select(enclosingDivID_selector).node();
const element = root.select(enclosingDivIdSelector).node();
const diagramType = diag.type;
// -------------------------------------------------------------------------------
@@ -473,7 +473,7 @@ const render = async function (
}
// This is the d3 node for the svg element
const svgNode = root.select(`${enclosingDivID_selector} svg`);
const svgNode = root.select(`${enclosingDivIdSelector} svg`);
const a11yTitle: string | undefined = diag.db.getAccTitle?.();
const a11yDescr: string | undefined = diag.db.getAccDescription?.();
addA11yInfo(diagramType, svgNode, a11yTitle, a11yDescr);
@@ -482,13 +482,13 @@ const render = async function (
root.select(`[id="${id}"]`).selectAll('foreignobject > *').attr('xmlns', XMLNS_XHTML_STD);
// Fix for when the base tag is used
let svgCode: string = root.select(enclosingDivID_selector).node().innerHTML;
let svgCode: string = root.select(enclosingDivIdSelector).node().innerHTML;
log.debug('config.arrowMarkerAbsolute', config.arrowMarkerAbsolute);
svgCode = cleanUpSvgCode(svgCode, isSandboxed, evaluate(config.arrowMarkerAbsolute));
if (isSandboxed) {
const svgEl = root.select(enclosingDivID_selector + ' svg').node();
const svgEl = root.select(enclosingDivIdSelector + ' svg').node();
svgCode = putIntoIFrame(svgCode, svgEl);
} else if (!isLooseSecurityLevel) {
// Sanitize the svgCode using DOMPurify

View File

@@ -74,6 +74,7 @@ export class MockedD3 {
attr(attrName: string): undefined | string;
attr(attrName: string, attrValue: string): MockedD3;
attr(attrName: string, attrValue?: string): undefined | string | MockedD3 {
// biome-ignore lint/style/noArguments: <explanation>
if (arguments.length === 1) {
return this.attribs.get(attrName);
} else {
@@ -105,8 +106,8 @@ export class MockedD3 {
// Real implementation returns an HTML Element
public node = vi.fn().mockImplementation(() => {
const topElem = this._containingHTMLdoc.createElement('svg');
const elem_svgChild = this._containingHTMLdoc.createElement('svg'); // another svg element
topElem.appendChild(elem_svgChild);
const elemSvgChild = this._containingHTMLdoc.createElement('svg'); // another svg element
topElem.appendChild(elemSvgChild);
return topElem;
});

View File

@@ -11,51 +11,51 @@ addDiagrams();
describe('when assignWithDepth: should merge objects within objects', function () {
it('should handle simple, depth:1 types (identity)', function () {
const config_0 = { foo: 'bar', bar: 0 };
const config_1 = { foo: 'bar', bar: 0 };
const result = assignWithDepth(config_0, config_1);
expect(result).toEqual(config_1);
const config0 = { foo: 'bar', bar: 0 };
const config1 = { foo: 'bar', bar: 0 };
const result = assignWithDepth(config0, config1);
expect(result).toEqual(config1);
});
it('should handle simple, depth:1 types (dst: undefined)', function () {
const config_0 = undefined;
const config_1 = { foo: 'bar', bar: 0 };
const result = assignWithDepth(config_0, config_1);
expect(result).toEqual(config_1);
const config0 = undefined;
const config1 = { foo: 'bar', bar: 0 };
const result = assignWithDepth(config0, config1);
expect(result).toEqual(config1);
});
it('should handle simple, depth:1 types (src: undefined)', function () {
const config_0 = { foo: 'bar', bar: 0 };
const config_1 = undefined;
const result = assignWithDepth(config_0, config_1);
expect(result).toEqual(config_0);
const config0 = { foo: 'bar', bar: 0 };
const config1 = undefined;
const result = assignWithDepth(config0, config1);
expect(result).toEqual(config0);
});
it('should handle simple, depth:1 types (merge)', function () {
const config_0 = { foo: 'bar', bar: 0 };
const config_1 = { foo: 'foo' };
const result = assignWithDepth(config_0, config_1);
const config0 = { foo: 'bar', bar: 0 };
const config1 = { foo: 'foo' };
const result = assignWithDepth(config0, config1);
expect(result).toEqual({ foo: 'foo', bar: 0 });
});
it('should handle depth:2 types (dst: orphan)', function () {
const config_0 = { foo: 'bar', bar: { foo: 'bar' } };
const config_1 = { foo: 'bar' };
const result = assignWithDepth(config_0, config_1);
expect(result).toEqual(config_0);
const config0 = { foo: 'bar', bar: { foo: 'bar' } };
const config1 = { foo: 'bar' };
const result = assignWithDepth(config0, config1);
expect(result).toEqual(config0);
});
it('should handle depth:2 types (dst: object, src: simple type)', function () {
const config_0 = { foo: 'bar', bar: { foo: 'bar' } };
const config_1 = { foo: 'foo', bar: 'should NOT clobber' };
const result = assignWithDepth(config_0, config_1);
const config0 = { foo: 'bar', bar: { foo: 'bar' } };
const config1 = { foo: 'foo', bar: 'should NOT clobber' };
const result = assignWithDepth(config0, config1);
expect(result).toEqual({ foo: 'foo', bar: { foo: 'bar' } });
});
it('should handle depth:2 types (src: orphan)', function () {
const config_0 = { foo: 'bar' };
const config_1 = { foo: 'bar', bar: { foo: 'bar' } };
const result = assignWithDepth(config_0, config_1);
expect(result).toEqual(config_1);
const config0 = { foo: 'bar' };
const config1 = { foo: 'bar', bar: { foo: 'bar' } };
const result = assignWithDepth(config0, config1);
expect(result).toEqual(config1);
});
it('should handle depth:2 types (merge)', function () {
const config_0 = { foo: 'bar', bar: { foo: 'bar' }, boofar: 1 };
const config_1 = { foo: 'foo', bar: { bar: 0 }, foobar: 'foobar' };
const result = assignWithDepth(config_0, config_1);
const config0 = { foo: 'bar', bar: { foo: 'bar' }, boofar: 1 };
const config1 = { foo: 'foo', bar: { bar: 0 }, foobar: 'foobar' };
const result = assignWithDepth(config0, config1);
expect(result).toEqual({
foo: 'foo',
bar: { foo: 'bar', bar: 0 },
@@ -64,17 +64,17 @@ describe('when assignWithDepth: should merge objects within objects', function (
});
});
it('should handle depth:3 types (merge with clobber because assignWithDepth::depth == 2)', function () {
const config_0 = {
const config0 = {
foo: 'bar',
bar: { foo: 'bar', bar: { foo: { message: 'this', willbe: 'clobbered' } } },
boofar: 1,
};
const config_1 = {
const config1 = {
foo: 'foo',
bar: { foo: 'foo', bar: { foo: { message: 'clobbered other foo' } } },
foobar: 'foobar',
};
const result = assignWithDepth(config_0, config_1);
const result = assignWithDepth(config0, config1);
expect(result).toEqual({
foo: 'foo',
bar: { foo: 'foo', bar: { foo: { message: 'clobbered other foo' } } },
@@ -83,7 +83,7 @@ describe('when assignWithDepth: should merge objects within objects', function (
});
});
it('should handle depth:3 types (merge with clobber because assignWithDepth::depth == 1)', function () {
const config_0 = {
const config0 = {
foo: 'bar',
bar: {
foo: 'bar',
@@ -91,12 +91,12 @@ describe('when assignWithDepth: should merge objects within objects', function (
},
boofar: 1,
};
const config_1 = {
const config1 = {
foo: 'foo',
bar: { foo: 'foo', bar: { foo: { message: 'this' } } },
foobar: 'foobar',
};
const result = assignWithDepth(config_0, config_1, { depth: 1 });
const result = assignWithDepth(config0, config1, { depth: 1 });
expect(result).toEqual({
foo: 'foo',
bar: { foo: 'foo', bar: { foo: { message: 'this' } } },
@@ -105,17 +105,17 @@ describe('when assignWithDepth: should merge objects within objects', function (
});
});
it('should handle depth:3 types (merge with no clobber because assignWithDepth::depth == 3)', function () {
const config_0 = {
const config0 = {
foo: 'bar',
bar: { foo: 'bar', bar: { foo: { message: '', willbe: 'present' } } },
boofar: 1,
};
const config_1 = {
const config1 = {
foo: 'foo',
bar: { foo: 'foo', bar: { foo: { message: 'this' } } },
foobar: 'foobar',
};
const result = assignWithDepth(config_0, config_1, { depth: 3 });
const result = assignWithDepth(config0, config1, { depth: 3 });
expect(result).toEqual({
foo: 'foo',
bar: { foo: 'foo', bar: { foo: { message: 'this', willbe: 'present' } } },

View File

@@ -173,6 +173,7 @@ export const detectDirective = function (
);
let match: RegExpExecArray | null;
const result: Directive[] = [];
// biome-ignore lint/suspicious/noAssignInExpressions: <explanation>
while ((match = directiveRegex.exec(text)) !== null) {
// This is necessary to avoid infinite loops with zero-width matches
if (match.index === directiveRegex.lastIndex) {
@@ -443,7 +444,10 @@ function calcTerminalLabelPosition(
* @param arr - Declarations
* @returns The styles grouped as strings
*/
export function getStylesFromArray(arr: string[]): { style: string; labelStyle: string } {
export function getStylesFromArray(arr: string[]): {
style: string;
labelStyle: string;
} {
let style = '';
let labelStyle = '';

View File

@@ -0,0 +1,6 @@
export function toBase64(str: string) {
// ref: https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem
const utf8Bytes = new TextEncoder().encode(str);
const utf8Str = Array.from(utf8Bytes, (byte) => String.fromCodePoint(byte)).join('');
return btoa(utf8Str);
}

View File

@@ -3,7 +3,7 @@ import * as configApi from '../config.js';
describe('getSubGraphTitleMargins', () => {
it('should get subgraph title margins after config has been set', () => {
const config_0 = {
const config0 = {
flowchart: {
subGraphTitleMargin: {
top: 10,
@@ -12,8 +12,8 @@ describe('getSubGraphTitleMargins', () => {
},
};
configApi.setSiteConfig(config_0);
expect(getSubGraphTitleMargins(config_0)).toEqual({
configApi.setSiteConfig(config0);
expect(getSubGraphTitleMargins(config0)).toEqual({
subGraphTitleTopMargin: 10,
subGraphTitleBottomMargin: 5,
subGraphTitleTotalMargin: 15,

View File

@@ -3,10 +3,7 @@
"version": "0.1.0-rc.1",
"description": "MermaidJS parser",
"author": "Yokozuna59",
"contributors": [
"Yokozuna59",
"Sidharth Vinod (https://sidharth.dev)"
],
"contributors": ["Yokozuna59", "Sidharth Vinod (https://sidharth.dev)"],
"homepage": "https://github.com/mermaid-js/mermaid/tree/develop/packages/mermaid/parser/#readme",
"types": "dist/src/index.d.ts",
"type": "module",
@@ -28,20 +25,14 @@
"directory": "packages/parser"
},
"license": "MIT",
"keywords": [
"mermaid",
"parser",
"ast"
],
"keywords": ["mermaid", "parser", "ast"],
"dependencies": {
"langium": "3.0.0"
},
"devDependencies": {
"chevrotain": "^11.0.3"
},
"files": [
"dist/"
],
"files": ["dist/"],
"publishConfig": {
"access": "public"
}