Merge branch 'develop' into next

* develop: (70 commits)
  build(deps-dev): bump vite from 4.4.9 to 4.4.12
  Changes to .prettierignore 1. Added 'demos/dev/**' to be ignored by Prettier. 2. Added '!/demos/dev/example.html' so that Prettier ensures no one changes the example.html in a way that doesn't obey the Prettier code formatting rules.
  build: use `tsx` instead of `ts-node-esm`
  chore: Downgrade node to 18.18.2
  fix: #5100 Add viewbox to sankey
  chore(deps): update all minor dependencies
  chore: Rename test
  test: Add unit test for generic classname and namespace
  fix: Check if parentCommit is provided
  Split type from generic class name
  Condition of Parent Id Without Merge Commit Added
  Referenced the PmWiki's Cookbook recipe enabling MermaidJs schematics in wiki pages
  test(e2e): fix pie chart E2E tests for PR #4288
  Add dummy commit to trigger GH checks
  chore: Revert unnecessary export
  refactor: Remove unnecessary calculations
  chore: Fix computeWidth function
  chore: Cleanup setupGraphViewbox
  Update docs
  update mermaidAPI to cleanup the text before passing to getDiagramFromText
  ...
This commit is contained in:
Sidharth Vinod 2023-12-06 20:50:04 +05:30
commit 000b432bb2
No known key found for this signature in database
GPG Key ID: FB5CCD378D3907CD
43 changed files with 1756 additions and 1135 deletions

View File

@ -6,6 +6,6 @@ export default {
// https://prettier.io/docs/en/cli.html#--cache // https://prettier.io/docs/en/cli.html#--cache
'prettier --write', 'prettier --write',
], ],
'cSpell.json': ['ts-node-esm scripts/fixCSpell.ts'], 'cSpell.json': ['tsx scripts/fixCSpell.ts'],
'**/*.jison': ['pnpm -w run lint:jison'], '**/*.jison': ['pnpm -w run lint:jison'],
}; };

View File

@ -10,6 +10,8 @@ stats
.nyc_output .nyc_output
# Autogenerated by `pnpm run --filter mermaid types:build-config` # Autogenerated by `pnpm run --filter mermaid types:build-config`
packages/mermaid/src/config.type.ts packages/mermaid/src/config.type.ts
# autogenereated by langium-cli # autogenereated by langium-cli
generated/ generated/
# Ignore the files creates in /demos/dev except for example.html
demos/dev/**
!/demos/dev/example.html

View File

@ -74,6 +74,9 @@ export const getBuildConfig = ({ minify, core, watch, entryName }: BuildOptions)
output, output,
}, },
}, },
define: {
'import.meta.vitest': 'undefined',
},
resolve: { resolve: {
extensions: [], extensions: [],
}, },

3
.vscode/launch.json vendored
View File

@ -18,7 +18,8 @@
"type": "node", "type": "node",
"request": "launch", "request": "launch",
"args": ["scripts/docs.cli.mts"], "args": ["scripts/docs.cli.mts"],
"runtimeArgs": ["--loader", "ts-node/esm"], // we'll need to change this to --import in Node.JS v20.6.0 and up
"runtimeArgs": ["--loader", "tsx/esm"],
"cwd": "${workspaceRoot}/packages/mermaid", "cwd": "${workspaceRoot}/packages/mermaid",
"skipFiles": ["<node_internals>/**", "**/node_modules/**"], "skipFiles": ["<node_internals>/**", "**/node_modules/**"],
"smartStep": true, "smartStep": true,

View File

@ -729,6 +729,18 @@ A ~~~ B
{} {}
); );
}); });
it('5064: Should render when subgraph child has links to outside node and subgraph', () => {
imgSnapshotTest(
`flowchart TB
Out --> In
subgraph Sub
In
end
Sub --> In`
);
});
describe('Markdown strings flowchart (#4220)', () => { describe('Markdown strings flowchart (#4220)', () => {
describe('html labels', () => { describe('html labels', () => {
it('With styling and classes', () => { it('With styling and classes', () => {
@ -874,4 +886,93 @@ end
}); });
}); });
}); });
describe('Subgraph title margins', () => {
it('Should render subgraphs with title margins set (LR)', () => {
imgSnapshotTest(
`flowchart LR
subgraph TOP
direction TB
subgraph B1
direction RL
i1 -->f1
end
subgraph B2
direction BT
i2 -->f2
end
end
A --> TOP --> B
B1 --> B2
`,
{ flowchart: { subGraphTitleMargin: { top: 10, bottom: 5 } } }
);
});
it('Should render subgraphs with title margins set (TD)', () => {
imgSnapshotTest(
`flowchart TD
subgraph TOP
direction LR
subgraph B1
direction RL
i1 -->f1
end
subgraph B2
direction BT
i2 -->f2
end
end
A --> TOP --> B
B1 --> B2
`,
{ flowchart: { subGraphTitleMargin: { top: 8, bottom: 16 } } }
);
});
it('Should render subgraphs with title margins set (LR) and htmlLabels set to false', () => {
imgSnapshotTest(
`flowchart LR
subgraph TOP
direction TB
subgraph B1
direction RL
i1 -->f1
end
subgraph B2
direction BT
i2 -->f2
end
end
A --> TOP --> B
B1 --> B2
`,
{
htmlLabels: false,
flowchart: { htmlLabels: false, subGraphTitleMargin: { top: 10, bottom: 5 } },
}
);
});
it('Should render subgraphs with title margins and edge labels', () => {
imgSnapshotTest(
`flowchart LR
subgraph TOP
direction TB
subgraph B1
direction RL
i1 --lb1-->f1
end
subgraph B2
direction BT
i2 --lb2-->f2
end
end
A --lb3--> TOP --lb4--> B
B1 --lb5--> B2
`,
{ flowchart: { subGraphTitleMargin: { top: 10, bottom: 5 } } }
);
});
});
}); });

View File

@ -811,4 +811,19 @@ gitGraph TB:
{} {}
); );
}); });
it('40: should render a simple gitgraph with cherry pick merge commit', () => {
imgSnapshotTest(
`gitGraph
commit id: "ZERO"
branch feature
branch release
checkout feature
commit id: "A"
commit id: "B"
checkout main
merge feature id: "M"
checkout release
cherry-pick id: "M" parent:"B"`
);
});
}); });

View File

@ -44,7 +44,7 @@ describe('pie chart', () => {
const style = svg.attr('style'); const style = svg.attr('style');
expect(style).to.match(/^max-width: [\d.]+px;$/); expect(style).to.match(/^max-width: [\d.]+px;$/);
const maxWidthValue = parseFloat(style.match(/[\d.]+/g).join('')); const maxWidthValue = parseFloat(style.match(/[\d.]+/g).join(''));
expect(maxWidthValue).to.eq(984); expect(maxWidthValue).to.be.within(590, 600); // depends on installed fonts: 596.2 on my PC, 597.5 on CI
}); });
}); });
@ -59,7 +59,7 @@ describe('pie chart', () => {
); );
cy.get('svg').should((svg) => { cy.get('svg').should((svg) => {
const width = parseFloat(svg.attr('width')); const width = parseFloat(svg.attr('width'));
expect(width).to.eq(984); expect(width).to.be.within(590, 600); // depends on installed fonts: 596.2 on my PC, 597.5 on CI
expect(svg).to.not.have.attr('style'); expect(svg).to.not.have.attr('style');
}); });
}); });

View File

@ -33,6 +33,7 @@
--- ---
config: config:
sankey: sankey:
useMaxWidth: true
showValues: false showValues: false
width: 1200 width: 1200
height: 600 height: 600

View File

@ -96,7 +96,7 @@ mermaid.initialize(config);
#### Defined in #### Defined in
[mermaidAPI.ts:603](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L603) [mermaidAPI.ts:608](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L608)
## Functions ## Functions

View File

@ -111,6 +111,8 @@ Communication tools and platforms
### Wikis ### Wikis
- [PmWiki](https://www.pmwiki.org)
- [MermaidJs Cookbook recipe](https://www.pmwiki.org/wiki/Cookbook/MermaidJs)
- [MediaWiki](https://www.mediawiki.org) - [MediaWiki](https://www.mediawiki.org)
- [Mermaid Extension](https://www.mediawiki.org/wiki/Extension:Mermaid) - [Mermaid Extension](https://www.mediawiki.org/wiki/Extension:Mermaid)
- [Flex Diagrams Extension](https://www.mediawiki.org/wiki/Extension:Flex_Diagrams) - [Flex Diagrams Extension](https://www.mediawiki.org/wiki/Extension:Flex_Diagrams)

View File

@ -366,6 +366,8 @@ A few important rules to note here are:
1. You need to provide the `id` for an existing commit to be cherry-picked. If given commit id does not exist it will result in an error. For this, make use of the `commit id:$value` format of declaring commits. See the examples from above. 1. You need to provide the `id` for an existing commit to be cherry-picked. If given commit id does not exist it will result in an error. For this, make use of the `commit id:$value` format of declaring commits. See the examples from above.
2. The given commit must not exist on the current branch. The cherry-picked commit must always be a different branch than the current branch. 2. The given commit must not exist on the current branch. The cherry-picked commit must always be a different branch than the current branch.
3. Current branch must have at least one commit, before you can cherry-pick, otherwise it will cause an error is throw. 3. Current branch must have at least one commit, before you can cherry-pick, otherwise it will cause an error is throw.
4. When cherry-picking a merge commit, providing a parent commit ID is mandatory. If the parent attribute is omitted or an invalid parent commit ID is provided, an error will be thrown.
5. The specified parent commit must be an immediate parent of the merge commit being cherry-picked.
Let see an example: Let see an example:
@ -373,14 +375,17 @@ Let see an example:
gitGraph gitGraph
commit id: "ZERO" commit id: "ZERO"
branch develop branch develop
branch release
commit id:"A" commit id:"A"
checkout main checkout main
commit id:"ONE" commit id:"ONE"
checkout develop checkout develop
commit id:"B" commit id:"B"
checkout main checkout main
merge develop id:"MERGE"
commit id:"TWO" commit id:"TWO"
cherry-pick id:"A" checkout release
cherry-pick id:"MERGE" parent:"B"
commit id:"THREE" commit id:"THREE"
checkout develop checkout develop
commit id:"C" commit id:"C"
@ -390,14 +395,17 @@ Let see an example:
gitGraph gitGraph
commit id: "ZERO" commit id: "ZERO"
branch develop branch develop
branch release
commit id:"A" commit id:"A"
checkout main checkout main
commit id:"ONE" commit id:"ONE"
checkout develop checkout develop
commit id:"B" commit id:"B"
checkout main checkout main
merge develop id:"MERGE"
commit id:"TWO" commit id:"TWO"
cherry-pick id:"A" checkout release
cherry-pick id:"MERGE" parent:"B"
commit id:"THREE" commit id:"THREE"
checkout develop checkout develop
commit id:"C" commit id:"C"

View File

@ -4,7 +4,7 @@
"version": "10.2.4", "version": "10.2.4",
"description": "Markdownish syntax for generating flowcharts, sequence diagrams, class diagrams, gantt charts and git graphs.", "description": "Markdownish syntax for generating flowcharts, sequence diagrams, class diagrams, gantt charts and git graphs.",
"type": "module", "type": "module",
"packageManager": "pnpm@8.10.5", "packageManager": "pnpm@8.11.0",
"keywords": [ "keywords": [
"diagram", "diagram",
"markdown", "markdown",
@ -16,25 +16,25 @@
], ],
"scripts": { "scripts": {
"build": "pnpm build:esbuild && pnpm build:types", "build": "pnpm build:esbuild && pnpm build:types",
"build:esbuild": "pnpm run -r clean && ts-node-esm --transpileOnly .esbuild/build.ts", "build:esbuild": "pnpm run -r clean && tsx .esbuild/build.ts",
"build:mermaid": "pnpm build:esbuild --mermaid", "build:mermaid": "pnpm build:esbuild --mermaid",
"build:viz": "pnpm build:esbuild --visualize", "build:viz": "pnpm build:esbuild --visualize",
"build:types": "ts-node-esm --transpileOnly .build/types.ts", "build:types": "tsx .build/types.ts",
"build:types:watch": "tsc -p ./packages/mermaid/tsconfig.json --emitDeclarationOnly --watch", "build:types:watch": "tsc -p ./packages/mermaid/tsconfig.json --emitDeclarationOnly --watch",
"dev": "ts-node-esm --transpileOnly .esbuild/server.ts", "dev": "tsx .esbuild/server.ts",
"dev:vite": "ts-node-esm --transpileOnly .vite/server.ts", "dev:vite": "tsx .vite/server.ts",
"dev:coverage": "pnpm coverage:cypress:clean && VITE_COVERAGE=true pnpm dev:vite", "dev:coverage": "pnpm coverage:cypress:clean && VITE_COVERAGE=true pnpm dev:vite",
"release": "pnpm build", "release": "pnpm build",
"lint": "eslint --cache --cache-strategy content --ignore-path .gitignore . && pnpm lint:jison && prettier --cache --check .", "lint": "eslint --cache --cache-strategy content --ignore-path .gitignore . && pnpm lint:jison && prettier --cache --check .",
"lint:fix": "eslint --cache --cache-strategy content --fix --ignore-path .gitignore . && prettier --write . && ts-node-esm scripts/fixCSpell.ts", "lint:fix": "eslint --cache --cache-strategy content --fix --ignore-path .gitignore . && prettier --write . && tsx scripts/fixCSpell.ts",
"lint:jison": "ts-node-esm ./scripts/jison/lint.mts", "lint:jison": "tsx ./scripts/jison/lint.mts",
"contributors": "ts-node-esm scripts/updateContributors.ts", "contributors": "tsx scripts/updateContributors.ts",
"cypress": "cypress run", "cypress": "cypress run",
"cypress:open": "cypress open", "cypress:open": "cypress open",
"e2e": "start-server-and-test dev http://localhost:9000/ cypress", "e2e": "start-server-and-test dev http://localhost:9000/ cypress",
"e2e:coverage": "start-server-and-test dev:coverage http://localhost:9000/ cypress", "e2e:coverage": "start-server-and-test dev:coverage http://localhost:9000/ cypress",
"coverage:cypress:clean": "rimraf .nyc_output coverage/cypress", "coverage:cypress:clean": "rimraf .nyc_output coverage/cypress",
"coverage:merge": "ts-node-esm scripts/coverage.ts", "coverage:merge": "tsx scripts/coverage.ts",
"coverage": "pnpm test:coverage --run && pnpm e2e:coverage && pnpm coverage:merge", "coverage": "pnpm test:coverage --run && pnpm e2e:coverage && pnpm coverage:merge",
"ci": "vitest run", "ci": "vitest run",
"test": "pnpm lint && vitest run", "test": "pnpm lint && vitest run",
@ -118,9 +118,9 @@
"rimraf": "^5.0.0", "rimraf": "^5.0.0",
"rollup-plugin-visualizer": "^5.9.2", "rollup-plugin-visualizer": "^5.9.2",
"start-server-and-test": "^2.0.0", "start-server-and-test": "^2.0.0",
"ts-node": "^10.9.1", "tsx": "^4.6.2",
"typescript": "^5.1.3", "typescript": "^5.1.3",
"vite": "^4.3.9", "vite": "^4.4.12",
"vite-plugin-istanbul": "^4.1.0", "vite-plugin-istanbul": "^4.1.0",
"vitest": "^0.34.0" "vitest": "^0.34.0"
}, },

View File

@ -11,6 +11,7 @@ import common from '../../mermaid/src/diagrams/common/common.js';
import { interpolateToCurve, getStylesFromArray } from '../../mermaid/src/utils.js'; import { interpolateToCurve, getStylesFromArray } from '../../mermaid/src/utils.js';
import ELK from 'elkjs/lib/elk.bundled.js'; import ELK from 'elkjs/lib/elk.bundled.js';
import { getLineFunctionsWithOffset } from '../../mermaid/src/utils/lineWithOffset.js'; import { getLineFunctionsWithOffset } from '../../mermaid/src/utils/lineWithOffset.js';
import { addEdgeMarkers } from '../../mermaid/src/dagre-wrapper/edgeMarker.js';
const elk = new ELK(); const elk = new ELK();
@ -586,108 +587,7 @@ const addMarkersToEdge = function (svgPath, edgeData, diagramType, arrowMarkerAb
} }
// look in edge data and decide which marker to use // look in edge data and decide which marker to use
switch (edgeData.arrowTypeStart) { addEdgeMarkers(svgPath, edgeData, url, id, diagramType);
case 'arrow_cross':
svgPath.attr(
'marker-start',
'url(' + url + '#' + id + '_' + diagramType + '-crossStart' + ')'
);
break;
case 'arrow_point':
svgPath.attr(
'marker-start',
'url(' + url + '#' + id + '_' + diagramType + '-pointStart' + ')'
);
break;
case 'arrow_barb':
svgPath.attr(
'marker-start',
'url(' + url + '#' + id + '_' + diagramType + '-barbStart' + ')'
);
break;
case 'arrow_circle':
svgPath.attr(
'marker-start',
'url(' + url + '#' + id + '_' + diagramType + '-circleStart' + ')'
);
break;
case 'aggregation':
svgPath.attr(
'marker-start',
'url(' + url + '#' + id + '_' + diagramType + '-aggregationStart' + ')'
);
break;
case 'extension':
svgPath.attr(
'marker-start',
'url(' + url + '#' + id + '_' + diagramType + '-extensionStart' + ')'
);
break;
case 'composition':
svgPath.attr(
'marker-start',
'url(' + url + '#' + id + '_' + diagramType + '-compositionStart' + ')'
);
break;
case 'dependency':
svgPath.attr(
'marker-start',
'url(' + url + '#' + id + '_' + diagramType + '-dependencyStart' + ')'
);
break;
case 'lollipop':
svgPath.attr(
'marker-start',
'url(' + url + '#' + id + '_' + diagramType + '-lollipopStart' + ')'
);
break;
default:
}
switch (edgeData.arrowTypeEnd) {
case 'arrow_cross':
svgPath.attr('marker-end', 'url(' + url + '#' + id + '_' + diagramType + '-crossEnd' + ')');
break;
case 'arrow_point':
svgPath.attr('marker-end', 'url(' + url + '#' + id + '_' + diagramType + '-pointEnd' + ')');
break;
case 'arrow_barb':
svgPath.attr('marker-end', 'url(' + url + '#' + id + '_' + diagramType + '-barbEnd' + ')');
break;
case 'arrow_circle':
svgPath.attr('marker-end', 'url(' + url + '#' + id + '_' + diagramType + '-circleEnd' + ')');
break;
case 'aggregation':
svgPath.attr(
'marker-end',
'url(' + url + '#' + id + '_' + diagramType + '-aggregationEnd' + ')'
);
break;
case 'extension':
svgPath.attr(
'marker-end',
'url(' + url + '#' + id + '_' + diagramType + '-extensionEnd' + ')'
);
break;
case 'composition':
svgPath.attr(
'marker-end',
'url(' + url + '#' + id + '_' + diagramType + '-compositionEnd' + ')'
);
break;
case 'dependency':
svgPath.attr(
'marker-end',
'url(' + url + '#' + id + '_' + diagramType + '-dependencyEnd' + ')'
);
break;
case 'lollipop':
svgPath.attr(
'marker-end',
'url(' + url + '#' + id + '_' + diagramType + '-lollipopEnd' + ')'
);
break;
default:
}
}; };
/** /**

View File

@ -26,18 +26,18 @@
"clean": "rimraf dist", "clean": "rimraf dist",
"dev": "pnpm -w dev", "dev": "pnpm -w dev",
"docs:code": "typedoc src/defaultConfig.ts src/config.ts src/mermaidAPI.ts && prettier --write ./src/docs/config/setup", "docs:code": "typedoc src/defaultConfig.ts src/config.ts src/mermaidAPI.ts && prettier --write ./src/docs/config/setup",
"docs:build": "rimraf ../../docs && pnpm docs:spellcheck && pnpm docs:code && ts-node-esm scripts/docs.cli.mts", "docs:build": "rimraf ../../docs && pnpm docs:spellcheck && pnpm docs:code && tsx scripts/docs.cli.mts",
"docs:verify": "pnpm docs:spellcheck && pnpm docs:code && ts-node-esm scripts/docs.cli.mts --verify", "docs:verify": "pnpm docs:spellcheck && pnpm docs:code && tsx scripts/docs.cli.mts --verify",
"docs:pre:vitepress": "pnpm --filter ./src/docs prefetch && rimraf src/vitepress && pnpm docs:code && ts-node-esm scripts/docs.cli.mts --vitepress && pnpm --filter ./src/vitepress install --no-frozen-lockfile --ignore-scripts", "docs:pre:vitepress": "pnpm --filter ./src/docs prefetch && rimraf src/vitepress && pnpm docs:code && tsx scripts/docs.cli.mts --vitepress && pnpm --filter ./src/vitepress install --no-frozen-lockfile --ignore-scripts",
"docs:build:vitepress": "pnpm docs:pre:vitepress && (cd src/vitepress && pnpm run build) && cpy --flat src/docs/landing/ ./src/vitepress/.vitepress/dist/landing", "docs:build:vitepress": "pnpm docs:pre:vitepress && (cd src/vitepress && pnpm run build) && cpy --flat src/docs/landing/ ./src/vitepress/.vitepress/dist/landing",
"docs:dev": "pnpm docs:pre:vitepress && concurrently \"pnpm --filter ./src/vitepress dev\" \"ts-node-esm scripts/docs.cli.mts --watch --vitepress\"", "docs:dev": "pnpm docs:pre:vitepress && concurrently \"pnpm --filter ./src/vitepress dev\" \"tsx scripts/docs.cli.mts --watch --vitepress\"",
"docs:dev:docker": "pnpm docs:pre:vitepress && concurrently \"pnpm --filter ./src/vitepress dev:docker\" \"ts-node-esm scripts/docs.cli.mts --watch --vitepress\"", "docs:dev:docker": "pnpm docs:pre:vitepress && concurrently \"pnpm --filter ./src/vitepress dev:docker\" \"tsx scripts/docs.cli.mts --watch --vitepress\"",
"docs:serve": "pnpm docs:build:vitepress && vitepress serve src/vitepress", "docs:serve": "pnpm docs:build:vitepress && vitepress serve src/vitepress",
"docs:spellcheck": "cspell --config ../../cSpell.json \"src/docs/**/*.md\"", "docs:spellcheck": "cspell --config ../../cSpell.json \"src/docs/**/*.md\"",
"docs:release-version": "ts-node-esm scripts/update-release-version.mts", "docs:release-version": "tsx scripts/update-release-version.mts",
"docs:verify-version": "ts-node-esm scripts/update-release-version.mts --verify", "docs:verify-version": "tsx scripts/update-release-version.mts --verify",
"types:build-config": "ts-node-esm --transpileOnly scripts/create-types-from-json-schema.mts", "types:build-config": "tsx scripts/create-types-from-json-schema.mts",
"types:verify-config": "ts-node-esm scripts/create-types-from-json-schema.mts --verify", "types:verify-config": "tsx scripts/create-types-from-json-schema.mts --verify",
"checkCircle": "npx madge --circular ./src", "checkCircle": "npx madge --circular ./src",
"release": "pnpm build", "release": "pnpm build",
"prepublishOnly": "cpy '../../README.*' ./ --cwd=. && pnpm docs:release-version && pnpm -w run build" "prepublishOnly": "cpy '../../README.*' ./ --cwd=. && pnpm docs:release-version && pnpm -w run build"

View File

@ -1396,6 +1396,14 @@ export interface FlowchartDiagramConfig extends BaseDiagramConfig {
* Margin top for the text over the diagram * Margin top for the text over the diagram
*/ */
titleTopMargin?: number; titleTopMargin?: number;
/**
* Defines a top/bottom margin for subgraph titles
*
*/
subGraphTitleMargin?: {
top?: number;
bottom?: number;
};
arrowMarkerAbsolute?: boolean; arrowMarkerAbsolute?: boolean;
/** /**
* The amount of padding around the diagram as a whole so that embedded * The amount of padding around the diagram as a whole so that embedded

View File

@ -5,9 +5,11 @@ import { createText } from '../rendering-util/createText.js';
import { select } from 'd3'; import { select } from 'd3';
import { getConfig } from '../diagram-api/diagramAPI.js'; import { getConfig } from '../diagram-api/diagramAPI.js';
import { evaluate } from '../diagrams/common/common.js'; import { evaluate } from '../diagrams/common/common.js';
import { getSubGraphTitleMargins } from '../utils/subGraphTitleMargins.js';
const rect = (parent, node) => { const rect = (parent, node) => {
log.info('Creating subgraph rect for ', node.id, node); log.info('Creating subgraph rect for ', node.id, node);
const siteConfig = getConfig();
// Add outer g element // Add outer g element
const shapeSvg = parent const shapeSvg = parent
@ -18,7 +20,7 @@ const rect = (parent, node) => {
// add the rect // add the rect
const rect = shapeSvg.insert('rect', ':first-child'); const rect = shapeSvg.insert('rect', ':first-child');
const useHtmlLabels = evaluate(getConfig().flowchart.htmlLabels); const useHtmlLabels = evaluate(siteConfig.flowchart.htmlLabels);
// Create the label and insert it after the rect // Create the label and insert it after the rect
const label = shapeSvg.insert('g').attr('class', 'cluster-label'); const label = shapeSvg.insert('g').attr('class', 'cluster-label');
@ -34,7 +36,7 @@ const rect = (parent, node) => {
// Get the size of the label // Get the size of the label
let bbox = text.getBBox(); let bbox = text.getBBox();
if (evaluate(getConfig().flowchart.htmlLabels)) { if (evaluate(siteConfig.flowchart.htmlLabels)) {
const div = text.children[0]; const div = text.children[0];
const dv = select(text); const dv = select(text);
bbox = div.getBoundingClientRect(); bbox = div.getBoundingClientRect();
@ -63,17 +65,18 @@ const rect = (parent, node) => {
.attr('width', width) .attr('width', width)
.attr('height', node.height + padding); .attr('height', node.height + padding);
const { subGraphTitleTopMargin } = getSubGraphTitleMargins(siteConfig);
if (useHtmlLabels) { if (useHtmlLabels) {
label.attr( label.attr(
'transform', 'transform',
// This puts the labal on top of the box instead of inside it // This puts the labal on top of the box instead of inside it
'translate(' + (node.x - bbox.width / 2) + ', ' + (node.y - node.height / 2) + ')' `translate(${node.x - bbox.width / 2}, ${node.y - node.height / 2 + subGraphTitleTopMargin})`
); );
} else { } else {
label.attr( label.attr(
'transform', 'transform',
// This puts the labal on top of the box instead of inside it // This puts the labal on top of the box instead of inside it
'translate(' + node.x + ', ' + (node.y - node.height / 2) + ')' `translate(${node.x}, ${node.y - node.height / 2 + subGraphTitleTopMargin})`
); );
} }
// Center the label // Center the label
@ -127,6 +130,8 @@ const noteGroup = (parent, node) => {
return shapeSvg; return shapeSvg;
}; };
const roundedWithTitle = (parent, node) => { const roundedWithTitle = (parent, node) => {
const siteConfig = getConfig();
// Add outer g element // Add outer g element
const shapeSvg = parent.insert('g').attr('class', node.classes).attr('id', node.id); const shapeSvg = parent.insert('g').attr('class', node.classes).attr('id', node.id);
@ -143,7 +148,7 @@ const roundedWithTitle = (parent, node) => {
// Get the size of the label // Get the size of the label
let bbox = text.getBBox(); let bbox = text.getBBox();
if (evaluate(getConfig().flowchart.htmlLabels)) { if (evaluate(siteConfig.flowchart.htmlLabels)) {
const div = text.children[0]; const div = text.children[0];
const dv = select(text); const dv = select(text);
bbox = div.getBoundingClientRect(); bbox = div.getBoundingClientRect();
@ -175,6 +180,7 @@ const roundedWithTitle = (parent, node) => {
.attr('width', width + padding) .attr('width', width + padding)
.attr('height', node.height + padding - bbox.height - 3); .attr('height', node.height + padding - bbox.height - 3);
const { subGraphTitleTopMargin } = getSubGraphTitleMargins(siteConfig);
// Center the label // Center the label
label.attr( label.attr(
'transform', 'transform',
@ -184,7 +190,8 @@ const roundedWithTitle = (parent, node) => {
(node.y - (node.y -
node.height / 2 - node.height / 2 -
node.padding / 3 + node.padding / 3 +
(evaluate(getConfig().flowchart.htmlLabels) ? 5 : 3)) + (evaluate(siteConfig.flowchart.htmlLabels) ? 5 : 3)) +
subGraphTitleTopMargin +
')' ')'
); );

View File

@ -0,0 +1,79 @@
import type { Mocked } from 'vitest';
import type { SVG } from '../diagram-api/types.js';
import { addEdgeMarkers } from './edgeMarker.js';
describe('addEdgeMarker', () => {
const svgPath = {
attr: vitest.fn(),
} as unknown as Mocked<SVG>;
const url = 'http://example.com';
const id = 'test';
const diagramType = 'test';
beforeEach(() => {
svgPath.attr.mockReset();
});
it('should add markers for arrow_cross:arrow_point', () => {
const arrowTypeStart = 'arrow_cross';
const arrowTypeEnd = 'arrow_point';
addEdgeMarkers(svgPath, { arrowTypeStart, arrowTypeEnd }, url, id, diagramType);
expect(svgPath.attr).toHaveBeenCalledWith(
'marker-start',
`url(${url}#${id}_${diagramType}-crossStart)`
);
expect(svgPath.attr).toHaveBeenCalledWith(
'marker-end',
`url(${url}#${id}_${diagramType}-pointEnd)`
);
});
it('should add markers for aggregation:arrow_point', () => {
const arrowTypeStart = 'aggregation';
const arrowTypeEnd = 'arrow_point';
addEdgeMarkers(svgPath, { arrowTypeStart, arrowTypeEnd }, url, id, diagramType);
expect(svgPath.attr).toHaveBeenCalledWith(
'marker-start',
`url(${url}#${id}_${diagramType}-aggregationStart)`
);
expect(svgPath.attr).toHaveBeenCalledWith(
'marker-end',
`url(${url}#${id}_${diagramType}-pointEnd)`
);
});
it('should add markers for arrow_point:aggregation', () => {
const arrowTypeStart = 'arrow_point';
const arrowTypeEnd = 'aggregation';
addEdgeMarkers(svgPath, { arrowTypeStart, arrowTypeEnd }, url, id, diagramType);
expect(svgPath.attr).toHaveBeenCalledWith(
'marker-start',
`url(${url}#${id}_${diagramType}-pointStart)`
);
expect(svgPath.attr).toHaveBeenCalledWith(
'marker-end',
`url(${url}#${id}_${diagramType}-aggregationEnd)`
);
});
it('should add markers for aggregation:composition', () => {
const arrowTypeStart = 'aggregation';
const arrowTypeEnd = 'composition';
addEdgeMarkers(svgPath, { arrowTypeStart, arrowTypeEnd }, url, id, diagramType);
expect(svgPath.attr).toHaveBeenCalledWith(
'marker-start',
`url(${url}#${id}_${diagramType}-aggregationStart)`
);
expect(svgPath.attr).toHaveBeenCalledWith(
'marker-end',
`url(${url}#${id}_${diagramType}-compositionEnd)`
);
});
it('should not add invalid markers', () => {
const arrowTypeStart = 'this is an invalid marker';
const arrowTypeEnd = ') url(https://my-malicious-site.example)';
addEdgeMarkers(svgPath, { arrowTypeStart, arrowTypeEnd }, url, id, diagramType);
expect(svgPath.attr).not.toHaveBeenCalled();
});
});

View File

@ -0,0 +1,57 @@
import type { SVG } from '../diagram-api/types.js';
import { log } from '../logger.js';
import type { EdgeData } from '../types.js';
/**
* Adds SVG markers to a path element based on the arrow types specified in the edge.
*
* @param svgPath - The SVG path element to add markers to.
* @param edge - The edge data object containing the arrow types.
* @param url - The URL of the SVG marker definitions.
* @param id - The ID prefix for the SVG marker definitions.
* @param diagramType - The type of diagram being rendered.
*/
export const addEdgeMarkers = (
svgPath: SVG,
edge: Pick<EdgeData, 'arrowTypeStart' | 'arrowTypeEnd'>,
url: string,
id: string,
diagramType: string
) => {
if (edge.arrowTypeStart) {
addEdgeMarker(svgPath, 'start', edge.arrowTypeStart, url, id, diagramType);
}
if (edge.arrowTypeEnd) {
addEdgeMarker(svgPath, 'end', edge.arrowTypeEnd, url, id, diagramType);
}
};
const arrowTypesMap = {
arrow_cross: 'cross',
arrow_point: 'point',
arrow_barb: 'barb',
arrow_circle: 'circle',
aggregation: 'aggregation',
extension: 'extension',
composition: 'composition',
dependency: 'dependency',
lollipop: 'lollipop',
} as const;
const addEdgeMarker = (
svgPath: SVG,
position: 'start' | 'end',
arrowType: string,
url: string,
id: string,
diagramType: string
) => {
const endMarkerType = arrowTypesMap[arrowType as keyof typeof arrowTypesMap];
if (!endMarkerType) {
log.warn(`Unknown arrow type: ${arrowType}`);
return; // unknown arrow type, ignore
}
const suffix = position === 'start' ? 'Start' : 'End';
svgPath.attr(`marker-${position}`, `url(${url}#${id}_${diagramType}-${endMarkerType}${suffix})`);
};

View File

@ -6,6 +6,8 @@ import { getConfig } from '../diagram-api/diagramAPI.js';
import utils from '../utils.js'; import utils from '../utils.js';
import { evaluate } from '../diagrams/common/common.js'; import { evaluate } from '../diagrams/common/common.js';
import { getLineFunctionsWithOffset } from '../utils/lineWithOffset.js'; import { getLineFunctionsWithOffset } from '../utils/lineWithOffset.js';
import { getSubGraphTitleMargins } from '../utils/subGraphTitleMargins.js';
import { addEdgeMarkers } from './edgeMarker.js';
let edgeLabels = {}; let edgeLabels = {};
let terminalLabels = {}; let terminalLabels = {};
@ -135,6 +137,8 @@ function setTerminalWidth(fo, value) {
export const positionEdgeLabel = (edge, paths) => { export const positionEdgeLabel = (edge, paths) => {
log.info('Moving label abc78 ', edge.id, edge.label, edgeLabels[edge.id]); log.info('Moving label abc78 ', edge.id, edge.label, edgeLabels[edge.id]);
let path = paths.updatedPath ? paths.updatedPath : paths.originalPath; let path = paths.updatedPath ? paths.updatedPath : paths.originalPath;
const siteConfig = getConfig();
const { subGraphTitleTotalMargin } = getSubGraphTitleMargins(siteConfig);
if (edge.label) { if (edge.label) {
const el = edgeLabels[edge.id]; const el = edgeLabels[edge.id];
let x = edge.x; let x = edge.x;
@ -158,7 +162,7 @@ export const positionEdgeLabel = (edge, paths) => {
y = pos.y; y = pos.y;
} }
} }
el.attr('transform', 'translate(' + x + ', ' + y + ')'); el.attr('transform', `translate(${x}, ${y + subGraphTitleTotalMargin / 2})`);
} }
//let path = paths.updatedPath ? paths.updatedPath : paths.originalPath; //let path = paths.updatedPath ? paths.updatedPath : paths.originalPath;
@ -172,7 +176,7 @@ export const positionEdgeLabel = (edge, paths) => {
x = pos.x; x = pos.x;
y = pos.y; y = pos.y;
} }
el.attr('transform', 'translate(' + x + ', ' + y + ')'); el.attr('transform', `translate(${x}, ${y})`);
} }
if (edge.startLabelRight) { if (edge.startLabelRight) {
const el = terminalLabels[edge.id].startRight; const el = terminalLabels[edge.id].startRight;
@ -188,7 +192,7 @@ export const positionEdgeLabel = (edge, paths) => {
x = pos.x; x = pos.x;
y = pos.y; y = pos.y;
} }
el.attr('transform', 'translate(' + x + ', ' + y + ')'); el.attr('transform', `translate(${x}, ${y})`);
} }
if (edge.endLabelLeft) { if (edge.endLabelLeft) {
const el = terminalLabels[edge.id].endLeft; const el = terminalLabels[edge.id].endLeft;
@ -200,7 +204,7 @@ export const positionEdgeLabel = (edge, paths) => {
x = pos.x; x = pos.x;
y = pos.y; y = pos.y;
} }
el.attr('transform', 'translate(' + x + ', ' + y + ')'); el.attr('transform', `translate(${x}, ${y})`);
} }
if (edge.endLabelRight) { if (edge.endLabelRight) {
const el = terminalLabels[edge.id].endRight; const el = terminalLabels[edge.id].endRight;
@ -212,7 +216,7 @@ export const positionEdgeLabel = (edge, paths) => {
x = pos.x; x = pos.x;
y = pos.y; y = pos.y;
} }
el.attr('transform', 'translate(' + x + ', ' + y + ')'); el.attr('transform', `translate(${x}, ${y})`);
} }
}; };
@ -506,108 +510,8 @@ export const insertEdge = function (elem, e, edge, clusterDb, diagramType, graph
log.info('arrowTypeStart', edge.arrowTypeStart); log.info('arrowTypeStart', edge.arrowTypeStart);
log.info('arrowTypeEnd', edge.arrowTypeEnd); log.info('arrowTypeEnd', edge.arrowTypeEnd);
switch (edge.arrowTypeStart) { addEdgeMarkers(svgPath, edge, url, id, diagramType);
case 'arrow_cross':
svgPath.attr(
'marker-start',
'url(' + url + '#' + id + '_' + diagramType + '-crossStart' + ')'
);
break;
case 'arrow_point':
svgPath.attr(
'marker-start',
'url(' + url + '#' + id + '_' + diagramType + '-pointStart' + ')'
);
break;
case 'arrow_barb':
svgPath.attr(
'marker-start',
'url(' + url + '#' + id + '_' + diagramType + '-barbStart' + ')'
);
break;
case 'arrow_circle':
svgPath.attr(
'marker-start',
'url(' + url + '#' + id + '_' + diagramType + '-circleStart' + ')'
);
break;
case 'aggregation':
svgPath.attr(
'marker-start',
'url(' + url + '#' + id + '_' + diagramType + '-aggregationStart' + ')'
);
break;
case 'extension':
svgPath.attr(
'marker-start',
'url(' + url + '#' + id + '_' + diagramType + '-extensionStart' + ')'
);
break;
case 'composition':
svgPath.attr(
'marker-start',
'url(' + url + '#' + id + '_' + diagramType + '-compositionStart' + ')'
);
break;
case 'dependency':
svgPath.attr(
'marker-start',
'url(' + url + '#' + id + '_' + diagramType + '-dependencyStart' + ')'
);
break;
case 'lollipop':
svgPath.attr(
'marker-start',
'url(' + url + '#' + id + '_' + diagramType + '-lollipopStart' + ')'
);
break;
default:
}
switch (edge.arrowTypeEnd) {
case 'arrow_cross':
svgPath.attr('marker-end', 'url(' + url + '#' + id + '_' + diagramType + '-crossEnd' + ')');
break;
case 'arrow_point':
svgPath.attr('marker-end', 'url(' + url + '#' + id + '_' + diagramType + '-pointEnd' + ')');
break;
case 'arrow_barb':
svgPath.attr('marker-end', 'url(' + url + '#' + id + '_' + diagramType + '-barbEnd' + ')');
break;
case 'arrow_circle':
svgPath.attr('marker-end', 'url(' + url + '#' + id + '_' + diagramType + '-circleEnd' + ')');
break;
case 'aggregation':
svgPath.attr(
'marker-end',
'url(' + url + '#' + id + '_' + diagramType + '-aggregationEnd' + ')'
);
break;
case 'extension':
svgPath.attr(
'marker-end',
'url(' + url + '#' + id + '_' + diagramType + '-extensionEnd' + ')'
);
break;
case 'composition':
svgPath.attr(
'marker-end',
'url(' + url + '#' + id + '_' + diagramType + '-compositionEnd' + ')'
);
break;
case 'dependency':
svgPath.attr(
'marker-end',
'url(' + url + '#' + id + '_' + diagramType + '-dependencyEnd' + ')'
);
break;
case 'lollipop':
svgPath.attr(
'marker-end',
'url(' + url + '#' + id + '_' + diagramType + '-lollipopEnd' + ')'
);
break;
default:
}
let paths = {}; let paths = {};
if (pointsHasChanged) { if (pointsHasChanged) {
paths.updatedPath = points; paths.updatedPath = points;

View File

@ -13,8 +13,10 @@ import { insertNode, positionNode, clear as clearNodes, setNodeElem } from './no
import { insertCluster, clear as clearClusters } from './clusters.js'; import { insertCluster, clear as clearClusters } from './clusters.js';
import { insertEdgeLabel, positionEdgeLabel, insertEdge, clear as clearEdges } from './edges.js'; import { insertEdgeLabel, positionEdgeLabel, insertEdge, clear as clearEdges } from './edges.js';
import { log } from '../logger.js'; import { log } from '../logger.js';
import { getSubGraphTitleMargins } from '../utils/subGraphTitleMargins.js';
import { getConfig } from '../diagram-api/diagramAPI.js';
const recursiveRender = async (_elem, graph, diagramtype, id, parentCluster) => { const recursiveRender = async (_elem, graph, diagramtype, id, parentCluster, siteConfig) => {
log.info('Graph in recursive render: XXX', graphlibJson.write(graph), parentCluster); log.info('Graph in recursive render: XXX', graphlibJson.write(graph), parentCluster);
const dir = graph.graph().rankdir; const dir = graph.graph().rankdir;
log.trace('Dir in recursive render - dir:', dir); log.trace('Dir in recursive render - dir:', dir);
@ -52,7 +54,14 @@ const recursiveRender = async (_elem, graph, diagramtype, id, parentCluster) =>
if (node && node.clusterNode) { if (node && node.clusterNode) {
// const children = graph.children(v); // const children = graph.children(v);
log.info('Cluster identified', v, node.width, graph.node(v)); log.info('Cluster identified', v, node.width, graph.node(v));
const o = await recursiveRender(nodes, node.graph, diagramtype, id, graph.node(v)); const o = await recursiveRender(
nodes,
node.graph,
diagramtype,
id,
graph.node(v),
siteConfig
);
const newEl = o.elem; const newEl = o.elem;
updateNodeBounds(node, newEl); updateNodeBounds(node, newEl);
node.diff = o.diff || 0; node.diff = o.diff || 0;
@ -101,6 +110,7 @@ const recursiveRender = async (_elem, graph, diagramtype, id, parentCluster) =>
log.info('Graph after layout:', graphlibJson.write(graph)); log.info('Graph after layout:', graphlibJson.write(graph));
// Move the nodes to the correct place // Move the nodes to the correct place
let diff = 0; let diff = 0;
const { subGraphTitleTotalMargin } = getSubGraphTitleMargins(siteConfig);
sortNodesByHierarchy(graph).forEach(function (v) { sortNodesByHierarchy(graph).forEach(function (v) {
const node = graph.node(v); const node = graph.node(v);
log.info('Position ' + v + ': ' + JSON.stringify(graph.node(v))); log.info('Position ' + v + ': ' + JSON.stringify(graph.node(v)));
@ -114,16 +124,18 @@ const recursiveRender = async (_elem, graph, diagramtype, id, parentCluster) =>
); );
if (node && node.clusterNode) { if (node && node.clusterNode) {
// clusterDb[node.id].node = node; // clusterDb[node.id].node = node;
node.y += subGraphTitleTotalMargin;
positionNode(node); positionNode(node);
} else { } else {
// Non cluster node // Non cluster node
if (graph.children(v).length > 0) { if (graph.children(v).length > 0) {
// A cluster in the non-recursive way // A cluster in the non-recursive way
// positionCluster(node); // positionCluster(node);
node.height += subGraphTitleTotalMargin;
insertCluster(clusters, node); insertCluster(clusters, node);
clusterDb[node.id].node = node; clusterDb[node.id].node = node;
} else { } else {
node.y += subGraphTitleTotalMargin / 2;
positionNode(node); positionNode(node);
} }
} }
@ -134,6 +146,7 @@ const recursiveRender = async (_elem, graph, diagramtype, id, parentCluster) =>
const edge = graph.edge(e); const edge = graph.edge(e);
log.info('Edge ' + e.v + ' -> ' + e.w + ': ' + JSON.stringify(edge), edge); log.info('Edge ' + e.v + ' -> ' + e.w + ': ' + JSON.stringify(edge), edge);
edge.points.forEach((point) => (point.y += subGraphTitleTotalMargin / 2));
const paths = insertEdge(edgePaths, e, edge, clusterDb, diagramtype, graph, id); const paths = insertEdge(edgePaths, e, edge, clusterDb, diagramtype, graph, id);
positionEdgeLabel(edge, paths); positionEdgeLabel(edge, paths);
}); });
@ -159,7 +172,8 @@ export const render = async (elem, graph, markers, diagramtype, id) => {
adjustClustersAndEdges(graph); adjustClustersAndEdges(graph);
log.warn('Graph after:', JSON.stringify(graphlibJson.write(graph))); log.warn('Graph after:', JSON.stringify(graphlibJson.write(graph)));
// log.warn('Graph ever after:', graphlibJson.write(graph.node('A').graph)); // log.warn('Graph ever after:', graphlibJson.write(graph.node('A').graph));
await recursiveRender(elem, graph, diagramtype, id); const siteConfig = getConfig();
await recursiveRender(elem, graph, diagramtype, id, undefined, siteConfig);
}; };
// const shapeDefinitions = {}; // const shapeDefinitions = {};

View File

@ -80,7 +80,9 @@ export const labelHelper = async (parent, node, _classes, isNode) => {
? getConfig().fontSize ? getConfig().fontSize
: window.getComputedStyle(document.body).fontSize; : window.getComputedStyle(document.body).fontSize;
const enlargingFactor = 5; const enlargingFactor = 5;
img.style.width = parseInt(bodyFontSize, 10) * enlargingFactor + 'px'; const width = parseInt(bodyFontSize, 10) * enlargingFactor + 'px';
img.style.minWidth = width;
img.style.maxWidth = width;
} else { } else {
img.style.width = '100%'; img.style.width = '100%';
} }

View File

@ -446,11 +446,13 @@ const getNamespaces = function (): NamespaceMap {
* @public * @public
*/ */
export const addClassesToNamespace = function (id: string, classNames: string[]) { export const addClassesToNamespace = function (id: string, classNames: string[]) {
if (namespaces[id] !== undefined) { if (namespaces[id] === undefined) {
classNames.map((className) => { return;
}
for (const name of classNames) {
const { className } = splitClassNameAndType(name);
classes[className].parent = id; classes[className].parent = id;
namespaces[id].classes[className] = classes[className]; namespaces[id].classes[className] = classes[className];
});
} }
}; };

View File

@ -1043,6 +1043,19 @@ foo()
`; `;
parser.parse(str); parser.parse(str);
}); });
it('should handle namespace with generic types', () => {
parser.parse(`classDiagram
namespace space {
class Square~Shape~{
int id
List~int~ position
setPoints(List~int~ points)
getPoints() List~int~
}
}`);
});
}); });
}); });

View File

@ -145,6 +145,7 @@ g.classGroup line {
.edgeTerminals { .edgeTerminals {
font-size: 11px; font-size: 11px;
line-height: initial;
} }
.classTitleText { .classTitleText {

View File

@ -255,11 +255,12 @@ export const merge = function (otherBranch, custom_id, override_type, custom_tag
log.debug('in mergeBranch'); log.debug('in mergeBranch');
}; };
export const cherryPick = function (sourceId, targetId, tag) { export const cherryPick = function (sourceId, targetId, tag, parentCommitId) {
log.debug('Entering cherryPick:', sourceId, targetId, tag); log.debug('Entering cherryPick:', sourceId, targetId, tag);
sourceId = common.sanitizeText(sourceId, getConfig()); sourceId = common.sanitizeText(sourceId, getConfig());
targetId = common.sanitizeText(targetId, getConfig()); targetId = common.sanitizeText(targetId, getConfig());
tag = common.sanitizeText(tag, getConfig()); tag = common.sanitizeText(tag, getConfig());
parentCommitId = common.sanitizeText(parentCommitId, getConfig());
if (!sourceId || commits[sourceId] === undefined) { if (!sourceId || commits[sourceId] === undefined) {
let error = new Error( let error = new Error(
@ -274,20 +275,21 @@ export const cherryPick = function (sourceId, targetId, tag) {
}; };
throw error; throw error;
} }
let sourceCommit = commits[sourceId]; let sourceCommit = commits[sourceId];
let sourceCommitBranch = sourceCommit.branch; let sourceCommitBranch = sourceCommit.branch;
if (sourceCommit.type === commitType.MERGE) { if (
parentCommitId &&
!(Array.isArray(sourceCommit.parents) && sourceCommit.parents.includes(parentCommitId))
) {
let error = new Error( let error = new Error(
'Incorrect usage of "cherryPick". Source commit should not be a merge commit' 'Invalid operation: The specified parent commit is not an immediate parent of the cherry-picked commit.'
);
throw error;
}
if (sourceCommit.type === commitType.MERGE && !parentCommitId) {
let error = new Error(
'Incorrect usage of cherry-pick: If the source commit is a merge commit, an immediate parent commit must be specified.'
); );
error.hash = {
text: 'cherryPick ' + sourceId + ' ' + targetId,
token: 'cherryPick ' + sourceId + ' ' + targetId,
line: '1',
loc: { first_line: 1, last_line: 1, first_column: 1, last_column: 1 },
expected: ['cherry-pick abc'],
};
throw error; throw error;
} }
if (!targetId || commits[targetId] === undefined) { if (!targetId || commits[targetId] === undefined) {
@ -327,7 +329,11 @@ export const cherryPick = function (sourceId, targetId, tag) {
parents: [head == null ? null : head.id, sourceCommit.id], parents: [head == null ? null : head.id, sourceCommit.id],
branch: curBranch, branch: curBranch,
type: commitType.CHERRY_PICK, type: commitType.CHERRY_PICK,
tag: tag ?? 'cherry-pick:' + sourceCommit.id, tag:
tag ??
`cherry-pick:${sourceCommit.id}${
sourceCommit.type === commitType.MERGE ? `|parent:${parentCommitId}` : ''
}`,
}; };
head = commit; head = commit;
commits[commit.id] = commit; commits[commit.id] = commit;

View File

@ -673,6 +673,145 @@ describe('when parsing a gitGraph', function () {
expect(commits[cherryPickCommitID].branch).toBe('main'); expect(commits[cherryPickCommitID].branch).toBe('main');
}); });
it('should support cherry-picking of merge commits', function () {
const str = `gitGraph
commit id: "ZERO"
branch feature
branch release
checkout feature
commit id: "A"
commit id: "B"
checkout main
merge feature id: "M"
checkout release
cherry-pick id: "M" parent:"B"
`;
parser.parse(str);
const commits = parser.yy.getCommits();
const cherryPickCommitID = Object.keys(commits)[4];
expect(commits[cherryPickCommitID].tag).toBe('cherry-pick:M|parent:B');
expect(commits[cherryPickCommitID].branch).toBe('release');
});
it('should support cherry-picking of merge commits with tag', function () {
const str = `gitGraph
commit id: "ZERO"
branch feature
branch release
checkout feature
commit id: "A"
commit id: "B"
checkout main
merge feature id: "M"
checkout release
cherry-pick id: "M" parent:"ZERO" tag: "v1.0"
`;
parser.parse(str);
const commits = parser.yy.getCommits();
const cherryPickCommitID = Object.keys(commits)[4];
expect(commits[cherryPickCommitID].tag).toBe('v1.0');
expect(commits[cherryPickCommitID].branch).toBe('release');
});
it('should support cherry-picking of merge commits with additional commit', function () {
const str = `gitGraph
commit id: "ZERO"
branch feature
branch release
checkout feature
commit id: "A"
commit id: "B"
checkout main
merge feature id: "M"
checkout release
commit id: "C"
cherry-pick id: "M" tag: "v2.1:ZERO" parent:"ZERO"
commit id: "D"
`;
parser.parse(str);
const commits = parser.yy.getCommits();
const cherryPickCommitID = Object.keys(commits)[5];
expect(commits[cherryPickCommitID].tag).toBe('v2.1:ZERO');
expect(commits[cherryPickCommitID].branch).toBe('release');
});
it('should support cherry-picking of merge commits with empty tag', function () {
const str = `gitGraph
commit id: "ZERO"
branch feature
branch release
checkout feature
commit id: "A"
commit id: "B"
checkout main
merge feature id: "M"
checkout release
commit id: "C"
cherry-pick id:"M" parent: "ZERO" tag:""
commit id: "D"
cherry-pick id:"M" tag:"" parent: "B"
`;
parser.parse(str);
const commits = parser.yy.getCommits();
const cherryPickCommitID = Object.keys(commits)[5];
const cherryPickCommitID2 = Object.keys(commits)[7];
expect(commits[cherryPickCommitID].tag).toBe('');
expect(commits[cherryPickCommitID2].tag).toBe('');
expect(commits[cherryPickCommitID].branch).toBe('release');
});
it('should fail cherry-picking of merge commits if the parent of merge commits is not specified', function () {
expect(() =>
parser
.parse(
`gitGraph
commit id: "ZERO"
branch feature
branch release
checkout feature
commit id: "A"
commit id: "B"
checkout main
merge feature id: "M"
checkout release
commit id: "C"
cherry-pick id:"M"
`
)
.toThrow(
'Incorrect usage of cherry-pick: If the source commit is a merge commit, an immediate parent commit must be specified.'
)
);
});
it('should fail cherry-picking of merge commits when the parent provided is not an immediate parent of cherry picked commit', function () {
expect(() =>
parser
.parse(
`gitGraph
commit id: "ZERO"
branch feature
branch release
checkout feature
commit id: "A"
commit id: "B"
checkout main
merge feature id: "M"
checkout release
commit id: "C"
cherry-pick id:"M" parent: "A"
`
)
.toThrow(
'Invalid operation: The specified parent commit is not an immediate parent of the cherry-picked commit.'
)
);
});
it('should throw error when try to branch existing branch: main', function () { it('should throw error when try to branch existing branch: main', function () {
const str = `gitGraph const str = `gitGraph
commit commit

View File

@ -39,6 +39,7 @@ branch(?=\s|$) return 'BRANCH';
"order:" return 'ORDER'; "order:" return 'ORDER';
merge(?=\s|$) return 'MERGE'; merge(?=\s|$) return 'MERGE';
cherry\-pick(?=\s|$) return 'CHERRY_PICK'; cherry\-pick(?=\s|$) return 'CHERRY_PICK';
"parent:" return 'PARENT_COMMIT'
// "reset" return 'RESET'; // "reset" return 'RESET';
checkout(?=\s|$) return 'CHECKOUT'; checkout(?=\s|$) return 'CHECKOUT';
"LR" return 'DIR'; "LR" return 'DIR';
@ -109,10 +110,17 @@ branchStatement
cherryPickStatement cherryPickStatement
: CHERRY_PICK COMMIT_ID STR {yy.cherryPick($3, '', undefined)} : CHERRY_PICK COMMIT_ID STR {yy.cherryPick($3, '', undefined)}
| CHERRY_PICK COMMIT_ID STR PARENT_COMMIT STR {yy.cherryPick($3, '', undefined,$5)}
| CHERRY_PICK COMMIT_ID STR COMMIT_TAG STR {yy.cherryPick($3, '', $5)} | CHERRY_PICK COMMIT_ID STR COMMIT_TAG STR {yy.cherryPick($3, '', $5)}
| CHERRY_PICK COMMIT_ID STR COMMIT_TAG EMPTYSTR {yy.cherryPick($3, '', '')} | CHERRY_PICK COMMIT_ID STR PARENT_COMMIT STR COMMIT_TAG STR {yy.cherryPick($3, '', $7,$5)}
| CHERRY_PICK COMMIT_ID STR COMMIT_TAG STR PARENT_COMMIT STR {yy.cherryPick($3, '', $5,$7)}
| CHERRY_PICK COMMIT_TAG STR COMMIT_ID STR {yy.cherryPick($5, '', $3)} | CHERRY_PICK COMMIT_TAG STR COMMIT_ID STR {yy.cherryPick($5, '', $3)}
| CHERRY_PICK COMMIT_TAG EMPTYSTR COMMIT_ID STR {yy.cherryPick($3, '', '')} | CHERRY_PICK COMMIT_TAG EMPTYSTR COMMIT_ID STR {yy.cherryPick($5, '', '')}
| CHERRY_PICK COMMIT_ID STR COMMIT_TAG EMPTYSTR {yy.cherryPick($3, '', '')}
| CHERRY_PICK COMMIT_ID STR PARENT_COMMIT STR COMMIT_TAG EMPTYSTR {yy.cherryPick($3, '', '',$5)}
| CHERRY_PICK COMMIT_ID STR COMMIT_TAG EMPTYSTR PARENT_COMMIT STR {yy.cherryPick($3, '', '',$7)}
| CHERRY_PICK COMMIT_TAG STR COMMIT_ID STR PARENT_COMMIT STR {yy.cherryPick($5, '', $3,$7)}
| CHERRY_PICK COMMIT_TAG EMPTYSTR COMMIT_ID STR PARENT_COMMIT STR{yy.cherryPick($5, '', '',$7)}
; ;
mergeStatement mergeStatement

View File

@ -1,6 +1,5 @@
import type d3 from 'd3'; import type d3 from 'd3';
import { scaleOrdinal, pie as d3pie, arc } from 'd3'; import { scaleOrdinal, pie as d3pie, arc } from 'd3';
import { log } from '../../logger.js'; import { log } from '../../logger.js';
import { configureSvgSize } from '../../setupGraphViewbox.js'; import { configureSvgSize } from '../../setupGraphViewbox.js';
import { getConfig } from '../../diagram-api/diagramAPI.js'; import { getConfig } from '../../diagram-api/diagramAPI.js';
@ -38,33 +37,25 @@ const createPieArcs = (sections: Sections): d3.PieArcDatum<D3Sections>[] => {
*/ */
export const draw: DrawDefinition = (text, id, _version, diagObj) => { export const draw: DrawDefinition = (text, id, _version, diagObj) => {
log.debug('rendering pie chart\n' + text); log.debug('rendering pie chart\n' + text);
const db = diagObj.db as PieDB; const db = diagObj.db as PieDB;
const globalConfig: MermaidConfig = getConfig(); const globalConfig: MermaidConfig = getConfig();
const pieConfig: Required<PieDiagramConfig> = cleanAndMerge(db.getConfig(), globalConfig.pie); 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 MARGIN = 40;
const LEGEND_RECT_SIZE = 18; const LEGEND_RECT_SIZE = 18;
const LEGEND_SPACING = 4; const LEGEND_SPACING = 4;
const height = 450;
const pieWidth: number = height;
const svg: SVG = selectSvgElement(id);
const group: Group = svg.append('g'); const group: Group = svg.append('g');
group.attr('transform', 'translate(' + width / 2 + ',' + height / 2 + ')'); const sections: Sections = db.getSections();
group.attr('transform', 'translate(' + pieWidth / 2 + ',' + height / 2 + ')');
const { themeVariables } = globalConfig; const { themeVariables } = globalConfig;
let [outerStrokeWidth] = parseFontSize(themeVariables.pieOuterStrokeWidth); let [outerStrokeWidth] = parseFontSize(themeVariables.pieOuterStrokeWidth);
outerStrokeWidth ??= 2; outerStrokeWidth ??= 2;
const textPosition: number = pieConfig.textPosition; const textPosition: number = pieConfig.textPosition;
const radius: number = Math.min(width, height) / 2 - MARGIN; const radius: number = Math.min(pieWidth, height) / 2 - MARGIN;
// Shape helper to build arcs: // Shape helper to build arcs:
const arcGenerator: d3.Arc<unknown, d3.PieArcDatum<D3Sections>> = arc< const arcGenerator: d3.Arc<unknown, d3.PieArcDatum<D3Sections>> = arc<
d3.PieArcDatum<D3Sections> d3.PieArcDatum<D3Sections>
@ -84,7 +75,6 @@ export const draw: DrawDefinition = (text, id, _version, diagObj) => {
.attr('r', radius + outerStrokeWidth / 2) .attr('r', radius + outerStrokeWidth / 2)
.attr('class', 'pieOuterCircle'); .attr('class', 'pieOuterCircle');
const sections: Sections = db.getSections();
const arcs: d3.PieArcDatum<D3Sections>[] = createPieArcs(sections); const arcs: d3.PieArcDatum<D3Sections>[] = createPieArcs(sections);
const myGeneratedColors = [ const myGeneratedColors = [
@ -177,6 +167,19 @@ export const draw: DrawDefinition = (text, id, _version, diagObj) => {
} }
return label; return label;
}); });
const longestTextWidth = Math.max(
...legend
.selectAll('text')
.nodes()
.map((node) => (node as Element)?.getBoundingClientRect().width ?? 0)
);
const totalWidth = pieWidth + MARGIN + LEGEND_RECT_SIZE + LEGEND_SPACING + longestTextWidth;
// Set viewBox
svg.attr('viewBox', `0 0 ${totalWidth} ${height}`);
configureSvgSize(svg, height, totalWidth, pieConfig.useMaxWidth);
}; };
export const renderer = { draw }; export const renderer = { draw };

View File

@ -16,7 +16,7 @@ import {
sankeyCenter as d3SankeyCenter, sankeyCenter as d3SankeyCenter,
sankeyJustify as d3SankeyJustify, sankeyJustify as d3SankeyJustify,
} from 'd3-sankey'; } from 'd3-sankey';
import { configureSvgSize } from '../../setupGraphViewbox.js'; import { setupGraphViewbox } from '../../setupGraphViewbox.js';
import { Uid } from '../../rendering-util/uid.js'; import { Uid } from '../../rendering-util/uid.js';
import type { SankeyNodeAlignment } from '../../config.type.js'; import type { SankeyNodeAlignment } from '../../config.type.js';
@ -70,12 +70,6 @@ export const draw = function (text: string, id: string, _version: string, diagOb
const suffix = conf?.suffix ?? defaultSankeyConfig.suffix!; const suffix = conf?.suffix ?? defaultSankeyConfig.suffix!;
const showValues = conf?.showValues ?? defaultSankeyConfig.showValues!; 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)`
//
// @ts-ignore TODO: svg type vs selection mismatch
configureSvgSize(svg, height, width, useMaxWidth);
// Prepare data for construction based on diagObj.db // Prepare data for construction based on diagObj.db
// This must be a mutable object with `nodes` and `links` properties: // This must be a mutable object with `nodes` and `links` properties:
// //
@ -208,6 +202,8 @@ export const draw = function (text: string, id: string, _version: string, diagOb
.attr('d', d3SankeyLinkHorizontal()) .attr('d', d3SankeyLinkHorizontal())
.attr('stroke', coloring) .attr('stroke', coloring)
.attr('stroke-width', (d: any) => Math.max(1, d.width)); .attr('stroke-width', (d: any) => Math.max(1, d.width));
setupGraphViewbox(undefined, svg, 0, useMaxWidth);
}; };
export default { export default {

View File

@ -109,6 +109,8 @@ Communication tools and platforms
### Wikis ### Wikis
- [PmWiki](https://www.pmwiki.org)
- [MermaidJs Cookbook recipe](https://www.pmwiki.org/wiki/Cookbook/MermaidJs)
- [MediaWiki](https://www.mediawiki.org) - [MediaWiki](https://www.mediawiki.org)
- [Mermaid Extension](https://www.mediawiki.org/wiki/Extension:Mermaid) - [Mermaid Extension](https://www.mediawiki.org/wiki/Extension:Mermaid)
- [Flex Diagrams Extension](https://www.mediawiki.org/wiki/Extension:Flex_Diagrams) - [Flex Diagrams Extension](https://www.mediawiki.org/wiki/Extension:Flex_Diagrams)

View File

@ -11,8 +11,8 @@
"preview-https": "pnpm build && serve .vitepress/dist", "preview-https": "pnpm build && serve .vitepress/dist",
"preview-https-no-prefetch": "pnpm build-no-prefetch && serve .vitepress/dist", "preview-https-no-prefetch": "pnpm build-no-prefetch && serve .vitepress/dist",
"prefetch": "pnpm fetch-contributors && pnpm fetch-avatars", "prefetch": "pnpm fetch-contributors && pnpm fetch-avatars",
"fetch-avatars": "ts-node-esm .vitepress/scripts/fetch-avatars.ts", "fetch-avatars": "tsx .vitepress/scripts/fetch-avatars.ts",
"fetch-contributors": "ts-node-esm .vitepress/scripts/fetch-contributors.ts" "fetch-contributors": "tsx .vitepress/scripts/fetch-contributors.ts"
}, },
"dependencies": { "dependencies": {
"@vueuse/core": "^10.1.0", "@vueuse/core": "^10.1.0",
@ -22,17 +22,17 @@
}, },
"devDependencies": { "devDependencies": {
"@iconify-json/carbon": "^1.1.16", "@iconify-json/carbon": "^1.1.16",
"@unocss/reset": "^0.57.0", "@unocss/reset": "^0.58.0",
"@vite-pwa/vitepress": "^0.3.0", "@vite-pwa/vitepress": "^0.3.0",
"@vitejs/plugin-vue": "^4.2.1", "@vitejs/plugin-vue": "^4.2.1",
"fast-glob": "^3.2.12", "fast-glob": "^3.2.12",
"https-localhost": "^4.7.1", "https-localhost": "^4.7.1",
"pathe": "^1.1.0", "pathe": "^1.1.0",
"unocss": "^0.57.0", "unocss": "^0.58.0",
"unplugin-vue-components": "^0.25.0", "unplugin-vue-components": "^0.26.0",
"vite": "^4.3.9", "vite": "^4.4.12",
"vite-plugin-pwa": "^0.17.0", "vite-plugin-pwa": "^0.17.0",
"vitepress": "1.0.0-rc.29", "vitepress": "1.0.0-rc.31",
"workbox-window": "^7.0.0" "workbox-window": "^7.0.0"
} }
} }

View File

@ -244,6 +244,8 @@ A few important rules to note here are:
1. You need to provide the `id` for an existing commit to be cherry-picked. If given commit id does not exist it will result in an error. For this, make use of the `commit id:$value` format of declaring commits. See the examples from above. 1. You need to provide the `id` for an existing commit to be cherry-picked. If given commit id does not exist it will result in an error. For this, make use of the `commit id:$value` format of declaring commits. See the examples from above.
2. The given commit must not exist on the current branch. The cherry-picked commit must always be a different branch than the current branch. 2. The given commit must not exist on the current branch. The cherry-picked commit must always be a different branch than the current branch.
3. Current branch must have at least one commit, before you can cherry-pick, otherwise it will cause an error is throw. 3. Current branch must have at least one commit, before you can cherry-pick, otherwise it will cause an error is throw.
4. When cherry-picking a merge commit, providing a parent commit ID is mandatory. If the parent attribute is omitted or an invalid parent commit ID is provided, an error will be thrown.
5. The specified parent commit must be an immediate parent of the merge commit being cherry-picked.
Let see an example: Let see an example:
@ -251,14 +253,17 @@ Let see an example:
gitGraph gitGraph
commit id: "ZERO" commit id: "ZERO"
branch develop branch develop
branch release
commit id:"A" commit id:"A"
checkout main checkout main
commit id:"ONE" commit id:"ONE"
checkout develop checkout develop
commit id:"B" commit id:"B"
checkout main checkout main
merge develop id:"MERGE"
commit id:"TWO" commit id:"TWO"
cherry-pick id:"A" checkout release
cherry-pick id:"MERGE" parent:"B"
commit id:"THREE" commit id:"THREE"
checkout develop checkout develop
commit id:"C" commit id:"C"

View File

@ -2,7 +2,7 @@ import mermaid from './mermaid.js';
import { mermaidAPI } from './mermaidAPI.js'; import { mermaidAPI } from './mermaidAPI.js';
import './diagram-api/diagram-orchestration.js'; import './diagram-api/diagram-orchestration.js';
import { addDiagrams } from './diagram-api/diagram-orchestration.js'; import { addDiagrams } from './diagram-api/diagram-orchestration.js';
import { beforeAll, describe, it, expect, vi } from 'vitest'; import { beforeAll, describe, it, expect, vi, afterEach } from 'vitest';
import type { DiagramDefinition } from './diagram-api/types.js'; import type { DiagramDefinition } from './diagram-api/types.js';
beforeAll(async () => { beforeAll(async () => {

View File

@ -67,6 +67,7 @@ vi.mock('stylis', () => {
}); });
import { compile, serialize } from 'stylis'; import { compile, serialize } from 'stylis';
import { decodeEntities, encodeEntities } from './utils.js'; import { decodeEntities, encodeEntities } from './utils.js';
import { Diagram } from './Diagram.js';
/** /**
* @see https://vitest.dev/guide/mocking.html Mock part of a module * @see https://vitest.dev/guide/mocking.html Mock part of a module
@ -744,4 +745,16 @@ describe('mermaidAPI', () => {
}); });
}); });
}); });
describe('getDiagramFromText', () => {
it('should clean up comments when present in diagram definition', async () => {
const diagram = await mermaidAPI.getDiagramFromText(
`flowchart LR
%% This is a comment A -- text --> B{node}
A -- text --> B -- text2 --> C`
);
expect(diagram).toBeInstanceOf(Diagram);
expect(diagram.type).toBe('flowchart-v2');
});
});
}); });

View File

@ -17,7 +17,7 @@ import { compile, serialize, stringify } from 'stylis';
import { version } from '../package.json'; import { version } from '../package.json';
import * as configApi from './config.js'; import * as configApi from './config.js';
import { addDiagrams } from './diagram-api/diagram-orchestration.js'; import { addDiagrams } from './diagram-api/diagram-orchestration.js';
import { Diagram, getDiagramFromText } from './Diagram.js'; import { Diagram, getDiagramFromText as getDiagramFromTextInternal } from './Diagram.js';
import errorRenderer from './diagrams/error/errorRenderer.js'; import errorRenderer from './diagrams/error/errorRenderer.js';
import { attachFunctions } from './interactionDb.js'; import { attachFunctions } from './interactionDb.js';
import { log, setLogLevel } from './logger.js'; import { log, setLogLevel } from './logger.js';
@ -28,7 +28,7 @@ import type { MermaidConfig } from './config.type.js';
import { evaluate } from './diagrams/common/common.js'; import { evaluate } from './diagrams/common/common.js';
import isEmpty from 'lodash-es/isEmpty.js'; import isEmpty from 'lodash-es/isEmpty.js';
import { setA11yDiagramInfo, addSVGa11yTitleDescription } from './accessibility.js'; import { setA11yDiagramInfo, addSVGa11yTitleDescription } from './accessibility.js';
import type { DiagramStyleClassDef } from './diagram-api/types.js'; import type { DiagramMetadata, DiagramStyleClassDef } from './diagram-api/types.js';
import { preprocessDiagram } from './preprocess.js'; import { preprocessDiagram } from './preprocess.js';
import { decodeEntities } from './utils.js'; import { decodeEntities } from './utils.js';
@ -519,6 +519,11 @@ function initialize(options: MermaidConfig = {}) {
addDiagrams(); addDiagrams();
} }
const getDiagramFromText = (text: string, metadata: Pick<DiagramMetadata, 'title'> = {}) => {
const { code } = preprocessDiagram(text);
return getDiagramFromTextInternal(code, metadata);
};
/** /**
* Add accessibility (a11y) information to the diagram. * Add accessibility (a11y) information to the diagram.
* *

View File

@ -1844,6 +1844,7 @@ $defs: # JSON Schema definition (maybe we should move these to a separate file)
unevaluatedProperties: false unevaluatedProperties: false
required: required:
- titleTopMargin - titleTopMargin
- subGraphTitleMargin
- diagramPadding - diagramPadding
- htmlLabels - htmlLabels
- nodeSpacing - nodeSpacing
@ -1856,6 +1857,20 @@ $defs: # JSON Schema definition (maybe we should move these to a separate file)
titleTopMargin: titleTopMargin:
$ref: '#/$defs/GitGraphDiagramConfig/properties/titleTopMargin' $ref: '#/$defs/GitGraphDiagramConfig/properties/titleTopMargin'
default: 25 default: 25
subGraphTitleMargin:
description: |
Defines a top/bottom margin for subgraph titles
type: object
properties:
top:
type: integer
minimum: 0
bottom:
type: integer
minimum: 0
default:
top: 0
bottom: 0
arrowMarkerAbsolute: arrowMarkerAbsolute:
type: boolean # TODO, is this actually used here (it has no default value but was in types) type: boolean # TODO, is this actually used here (it has no default value but was in types)
diagramPadding: diagramPadding:

View File

@ -44,6 +44,8 @@ export const configureSvgSize = function (svgElem, height, width, useMaxWidth) {
const attrs = calculateSvgSizeAttrs(height, width, useMaxWidth); const attrs = calculateSvgSizeAttrs(height, width, useMaxWidth);
d3Attrs(svgElem, attrs); d3Attrs(svgElem, attrs);
}; };
// TODO v11: Remove the graph parameter. It is not used.
export const setupGraphViewbox = function (graph, svgElem, padding, useMaxWidth) { export const setupGraphViewbox = function (graph, svgElem, padding, useMaxWidth) {
const svgBounds = svgElem.node().getBBox(); const svgBounds = svgElem.node().getBBox();
const sWidth = svgBounds.width; const sWidth = svgBounds.width;
@ -55,26 +57,13 @@ export const setupGraphViewbox = function (graph, svgElem, padding, useMaxWidth)
let height = 0; let height = 0;
log.info(`Graph bounds: ${width}x${height}`, graph); log.info(`Graph bounds: ${width}x${height}`, graph);
// let tx = 0;
// let ty = 0;
// if (sWidth > width) {
// tx = (sWidth - width) / 2 + padding;
width = sWidth + padding * 2; width = sWidth + padding * 2;
// } else {
// if (Math.abs(sWidth - width) >= 2 * padding + 1) {
// width = width - padding;
// }
// }
// if (sHeight > height) {
// ty = (sHeight - height) / 2 + padding;
height = sHeight + padding * 2; height = sHeight + padding * 2;
// }
log.info(`Calculated bounds: ${width}x${height}`); log.info(`Calculated bounds: ${width}x${height}`);
configureSvgSize(svgElem, height, width, useMaxWidth); configureSvgSize(svgElem, height, width, useMaxWidth);
// Ensure the viewBox includes the whole svgBounds area with extra space for padding // Ensure the viewBox includes the whole svgBounds area with extra space for padding
// const vBox = `0 0 ${width} ${height}`;
const vBox = `${svgBounds.x - padding} ${svgBounds.y - padding} ${ const vBox = `${svgBounds.x - padding} ${svgBounds.y - padding} ${
svgBounds.width + 2 * padding svgBounds.width + 2 * padding
} ${svgBounds.height + 2 * padding}`; } ${svgBounds.height + 2 * padding}`;

View File

@ -19,9 +19,12 @@ const markerOffsets = {
* @returns The angle, deltaX and deltaY * @returns The angle, deltaX and deltaY
*/ */
function calculateDeltaAndAngle( function calculateDeltaAndAngle(
point1: Point | [number, number], point1?: Point | [number, number],
point2: Point | [number, number] point2?: Point | [number, number]
): { angle: number; deltaX: number; deltaY: number } { ): { angle: number; deltaX: number; deltaY: number } {
if (point1 === undefined || point2 === undefined) {
return { angle: 0, deltaX: 0, deltaY: 0 };
}
point1 = pointTransformer(point1); point1 = pointTransformer(point1);
point2 = pointTransformer(point2); point2 = pointTransformer(point2);
const [x1, y1] = [point1.x, point1.y]; const [x1, y1] = [point1.x, point1.y];
@ -90,3 +93,44 @@ export const getLineFunctionsWithOffset = (
}, },
}; };
}; };
if (import.meta.vitest) {
const { it, expect, describe } = import.meta.vitest;
describe('calculateDeltaAndAngle', () => {
it('should calculate the angle and deltas between two points', () => {
expect(calculateDeltaAndAngle([0, 0], [0, 1])).toStrictEqual({
angle: 1.5707963267948966,
deltaX: 0,
deltaY: 1,
});
expect(calculateDeltaAndAngle([1, 0], [0, -1])).toStrictEqual({
angle: 0.7853981633974483,
deltaX: -1,
deltaY: -1,
});
expect(calculateDeltaAndAngle({ x: 1, y: 0 }, [0, -1])).toStrictEqual({
angle: 0.7853981633974483,
deltaX: -1,
deltaY: -1,
});
expect(calculateDeltaAndAngle({ x: 1, y: 0 }, { x: 1, y: 0 })).toStrictEqual({
angle: NaN,
deltaX: 0,
deltaY: 0,
});
});
it('should calculate the angle and deltas if one point in undefined', () => {
expect(calculateDeltaAndAngle(undefined, [0, 1])).toStrictEqual({
angle: 0,
deltaX: 0,
deltaY: 0,
});
expect(calculateDeltaAndAngle([0, 1], undefined)).toStrictEqual({
angle: 0,
deltaX: 0,
deltaY: 0,
});
});
});
}

View File

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

View File

@ -0,0 +1,21 @@
import type { FlowchartDiagramConfig } from '../config.type.js';
export const getSubGraphTitleMargins = ({
flowchart,
}: {
flowchart: FlowchartDiagramConfig;
}): {
subGraphTitleTopMargin: number;
subGraphTitleBottomMargin: number;
subGraphTitleTotalMargin: number;
} => {
const subGraphTitleTopMargin = flowchart?.subGraphTitleMargin?.top ?? 0;
const subGraphTitleBottomMargin = flowchart?.subGraphTitleMargin?.bottom ?? 0;
const subGraphTitleTotalMargin = subGraphTitleTopMargin + subGraphTitleBottomMargin;
return {
subGraphTitleTopMargin,
subGraphTitleBottomMargin,
subGraphTitleTotalMargin,
};
};

View File

@ -2,7 +2,8 @@
"extends": "../../tsconfig.json", "extends": "../../tsconfig.json",
"compilerOptions": { "compilerOptions": {
"rootDir": "./src", "rootDir": "./src",
"outDir": "./dist" "outDir": "./dist",
"types": ["vitest/importMeta", "vitest/globals"]
}, },
"include": ["./src/**/*.ts", "./package.json"] "include": ["./src/**/*.ts", "./package.json"]
} }

File diff suppressed because it is too large Load Diff

View File

@ -24,6 +24,7 @@ export default defineConfig({
reportsDirectory: './coverage/vitest', reportsDirectory: './coverage/vitest',
exclude: [...defaultExclude, './tests/**', '**/__mocks__/**', '**/generated/'], exclude: [...defaultExclude, './tests/**', '**/__mocks__/**', '**/generated/'],
}, },
includeSource: ['packages/*/src/**/*.{js,ts}'],
}, },
build: { build: {
/** If you set esmExternals to true, this plugins assumes that /** If you set esmExternals to true, this plugins assumes that
@ -33,4 +34,7 @@ export default defineConfig({
esmExternals: true, esmExternals: true,
}, },
}, },
define: {
'import.meta.vitest': 'undefined',
},
}); });