mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-11-11 16:24:12 +01:00
Merge branch 'develop' into sidv/examples
This commit is contained in:
5
.changeset/eleven-wolves-deny.md
Normal file
5
.changeset/eleven-wolves-deny.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'mermaid': patch
|
||||
---
|
||||
|
||||
chore: Convert StateDB into TypeScript
|
||||
5
.changeset/gold-shoes-camp.md
Normal file
5
.changeset/gold-shoes-camp.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'mermaid': patch
|
||||
---
|
||||
|
||||
fix: Remove incorrect `style="undefined;"` attributes in some Mermaid diagrams
|
||||
7
.changeset/honest-trees-dress.md
Normal file
7
.changeset/honest-trees-dress.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
'@mermaid-js/mermaid-zenuml': patch
|
||||
---
|
||||
|
||||
chore: bump minimum ZenUML version to 3.23.28
|
||||
|
||||
commit: 9d06d8f31e7f12af9e9e092214f907f2dc93ad75
|
||||
5
.changeset/neat-moose-compare.md
Normal file
5
.changeset/neat-moose-compare.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'mermaid': minor
|
||||
---
|
||||
|
||||
feat: Add support for styling Journey Diagram title (color, font-family, and font-size)
|
||||
6
.changeset/sad-mails-accept.md
Normal file
6
.changeset/sad-mails-accept.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'mermaid': patch
|
||||
'@mermaid-js/parser': patch
|
||||
---
|
||||
|
||||
Refactor grammar so that title don't break Architecture Diagrams
|
||||
5
.changeset/soft-readers-tan.md
Normal file
5
.changeset/soft-readers-tan.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'mermaid': minor
|
||||
---
|
||||
|
||||
feat: Dynamically Render Data Labels Within Bar Charts
|
||||
7
.changeset/yellow-mirrors-change.md
Normal file
7
.changeset/yellow-mirrors-change.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
'@mermaid-js/mermaid-zenuml': patch
|
||||
---
|
||||
|
||||
fix(zenuml): limit `peerDependencies` to Mermaid v10 and v11
|
||||
|
||||
commit: 0ad44c12feead9d20c6a870a49327ada58d6e657
|
||||
@@ -34,6 +34,19 @@ const buildPackage = async (entryName: keyof typeof packageOptions) => {
|
||||
{ ...iifeOptions, minify: true, metafile: shouldVisualize }
|
||||
);
|
||||
}
|
||||
if (entryName === 'mermaid-zenuml') {
|
||||
const iifeOptions: MermaidBuildOptions = {
|
||||
...commonOptions,
|
||||
format: 'iife',
|
||||
globalName: 'mermaid-zenuml',
|
||||
};
|
||||
buildConfigs.push(
|
||||
// mermaid-zenuml.js
|
||||
{ ...iifeOptions },
|
||||
// mermaid-zenuml.min.js
|
||||
{ ...iifeOptions, minify: true, metafile: shouldVisualize }
|
||||
);
|
||||
}
|
||||
|
||||
const results = await Promise.all(buildConfigs.map((option) => build(getBuildConfig(option))));
|
||||
|
||||
|
||||
@@ -58,6 +58,7 @@ export const getBuildConfig = (options: MermaidBuildOptions): BuildOptions => {
|
||||
format,
|
||||
minify,
|
||||
options: { name, file, packageName },
|
||||
globalName = 'mermaid',
|
||||
} = options;
|
||||
const external: string[] = ['require', 'fs', 'path'];
|
||||
const outFileName = getFileName(name, options);
|
||||
@@ -68,6 +69,7 @@ export const getBuildConfig = (options: MermaidBuildOptions): BuildOptions => {
|
||||
},
|
||||
metafile,
|
||||
minify,
|
||||
globalName,
|
||||
logLevel: 'info',
|
||||
chunkNames: `chunks/${outFileName}/[name]-[hash]`,
|
||||
define: {
|
||||
@@ -89,11 +91,12 @@ export const getBuildConfig = (options: MermaidBuildOptions): BuildOptions => {
|
||||
if (format === 'iife') {
|
||||
output.format = 'iife';
|
||||
output.splitting = false;
|
||||
output.globalName = '__esbuild_esm_mermaid';
|
||||
const originalGlobalName = output.globalName ?? 'mermaid';
|
||||
output.globalName = `__esbuild_esm_mermaid_nm[${JSON.stringify(originalGlobalName)}]`;
|
||||
// Workaround for removing the .default access in esbuild IIFE.
|
||||
// https://github.com/mermaid-js/mermaid/pull/4109#discussion_r1292317396
|
||||
output.footer = {
|
||||
js: 'globalThis.mermaid = globalThis.__esbuild_esm_mermaid.default;',
|
||||
js: `globalThis[${JSON.stringify(originalGlobalName)}] = globalThis.${output.globalName}.default;`,
|
||||
};
|
||||
output.outExtension = { '.js': '.js' };
|
||||
} else {
|
||||
|
||||
4
.github/lychee.toml
vendored
4
.github/lychee.toml
vendored
@@ -50,7 +50,9 @@ exclude = [
|
||||
"https://docs.swimm.io",
|
||||
|
||||
# Timeout
|
||||
"https://huehive.co"
|
||||
"https://huehive.co",
|
||||
"https://foswiki.org",
|
||||
"https://www.gnu.org",
|
||||
]
|
||||
|
||||
# Exclude all private IPs from checking.
|
||||
|
||||
15
.github/workflows/e2e-timings.yml
vendored
15
.github/workflows/e2e-timings.yml
vendored
@@ -30,6 +30,7 @@ jobs:
|
||||
uses: cypress-io/github-action@18a6541367f4580a515371905f499a27a44e8dbe # v6.7.12
|
||||
with:
|
||||
runTests: false
|
||||
|
||||
- name: Cypress run
|
||||
uses: cypress-io/github-action@18a6541367f4580a515371905f499a27a44e8dbe # v6.7.12
|
||||
id: cypress
|
||||
@@ -45,13 +46,25 @@ jobs:
|
||||
SPLIT: 1
|
||||
SPLIT_INDEX: 0
|
||||
SPLIT_FILE: 'cypress/timings.json'
|
||||
|
||||
- name: Compare timings
|
||||
id: compare
|
||||
run: |
|
||||
OUTPUT=$(pnpm tsx scripts/compare-timings.ts)
|
||||
echo "$OUTPUT" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
echo "output<<EOF" >> $GITHUB_OUTPUT
|
||||
echo "$OUTPUT" >> $GITHUB_OUTPUT
|
||||
echo "EOF" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Commit and create pull request
|
||||
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e
|
||||
uses: peter-evans/create-pull-request@a7b20e1da215b3ef3ccddb48ff65120256ed6226
|
||||
with:
|
||||
add-paths: |
|
||||
cypress/timings.json
|
||||
commit-message: 'chore: update E2E timings'
|
||||
branch: update-timings
|
||||
title: Update E2E Timings
|
||||
body: ${{ steps.compare.outputs.output }}
|
||||
delete-branch: true
|
||||
sign-commits: true
|
||||
|
||||
@@ -19,6 +19,25 @@ describe.skip('architecture diagram', () => {
|
||||
`
|
||||
);
|
||||
});
|
||||
it('should render a simple architecture diagram with titleAndAccessabilities', () => {
|
||||
imgSnapshotTest(
|
||||
`architecture-beta
|
||||
title Simple Architecture Diagram
|
||||
accTitle: Accessibility Title
|
||||
accDescr: Accessibility Description
|
||||
group api(cloud)[API]
|
||||
|
||||
service db(database)[Database] in api
|
||||
service disk1(disk)[Storage] in api
|
||||
service disk2(disk)[Storage] in api
|
||||
service server(server)[Server] in api
|
||||
|
||||
db:L -- R:server
|
||||
disk1:T -- B:server
|
||||
disk2:T -- B:db
|
||||
`
|
||||
);
|
||||
});
|
||||
it('should render an architecture diagram with groups within groups', () => {
|
||||
imgSnapshotTest(
|
||||
`architecture-beta
|
||||
@@ -172,7 +191,7 @@ describe.skip('architecture diagram', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should render an architecture diagram with a resonable height', () => {
|
||||
it('should render an architecture diagram with a reasonable height', () => {
|
||||
imgSnapshotTest(
|
||||
`architecture-beta
|
||||
group federated(cloud)[Federated Environment]
|
||||
|
||||
@@ -63,4 +63,199 @@ section Checkout from website
|
||||
{ journey: { useMaxWidth: false } }
|
||||
);
|
||||
});
|
||||
|
||||
it('should initialize with a left margin of 150px for user journeys', () => {
|
||||
renderGraph(
|
||||
`
|
||||
---
|
||||
config:
|
||||
journey:
|
||||
maxLabelWidth: 320
|
||||
---
|
||||
journey
|
||||
title User Journey Example
|
||||
section Onboarding
|
||||
Sign Up: 5:
|
||||
Browse Features: 3:
|
||||
Use Core Functionality: 4:
|
||||
section Engagement
|
||||
Browse Features: 3
|
||||
Use Core Functionality: 4
|
||||
`,
|
||||
{ journey: { useMaxWidth: true } }
|
||||
);
|
||||
|
||||
let diagramStartX;
|
||||
|
||||
cy.contains('foreignobject', 'Sign Up').then(($diagram) => {
|
||||
diagramStartX = parseFloat($diagram.attr('x'));
|
||||
expect(diagramStartX).to.be.closeTo(150, 2);
|
||||
});
|
||||
});
|
||||
|
||||
it('should maintain sufficient space between legend and diagram when legend labels are longer', () => {
|
||||
renderGraph(
|
||||
`journey
|
||||
title Web hook life cycle
|
||||
section Darkoob
|
||||
Make preBuilt:5: Darkoob user
|
||||
register slug : 5: Darkoob userf deliberately increasing the size of this label to check if distance between legend and diagram is maintained
|
||||
Map slug to a Prebuilt Job:5: Darkoob user
|
||||
section External Service
|
||||
set Darkoob slug as hook for an Event : 5 : admin Exjjjnjjjj qwerty
|
||||
listen to the events : 5 : External Service
|
||||
call darkoob endpoint : 5 : External Service
|
||||
section Darkoob
|
||||
check for inputs : 5 : DarkoobAPI
|
||||
run the prebuilt job : 5 : DarkoobAPI
|
||||
`,
|
||||
{ journey: { useMaxWidth: true } }
|
||||
);
|
||||
|
||||
let LabelEndX, diagramStartX;
|
||||
|
||||
// Get right edge of the legend
|
||||
cy.contains('tspan', 'Darkoob userf').then((textBox) => {
|
||||
const bbox = textBox[0].getBBox();
|
||||
LabelEndX = bbox.x + bbox.width;
|
||||
});
|
||||
|
||||
// Get left edge of the diagram
|
||||
cy.contains('foreignobject', 'Make preBuilt').then((rect) => {
|
||||
diagramStartX = parseFloat(rect.attr('x'));
|
||||
});
|
||||
|
||||
// Assert right edge of the diagram is greater than or equal to the right edge of the label
|
||||
cy.then(() => {
|
||||
expect(diagramStartX).to.be.gte(LabelEndX);
|
||||
});
|
||||
});
|
||||
|
||||
it('should wrap a single long word with hyphenation', () => {
|
||||
renderGraph(
|
||||
`
|
||||
---
|
||||
config:
|
||||
journey:
|
||||
maxLabelWidth: 100
|
||||
---
|
||||
journey
|
||||
title Long Word Test
|
||||
section Test
|
||||
VeryLongWord: 5: Supercalifragilisticexpialidocious
|
||||
`,
|
||||
{ journey: { useMaxWidth: true } }
|
||||
);
|
||||
|
||||
// Verify that the line ends with a hyphen, indicating proper hyphenation for words exceeding maxLabelWidth.
|
||||
cy.get('tspan').then((tspans) => {
|
||||
const hasHyphen = [...tspans].some((t) => t.textContent.trim().endsWith('-'));
|
||||
return expect(hasHyphen).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
it('should wrap text on whitespace without adding hyphens', () => {
|
||||
renderGraph(
|
||||
`
|
||||
---
|
||||
config:
|
||||
journey:
|
||||
maxLabelWidth: 200
|
||||
---
|
||||
journey
|
||||
title Whitespace Test
|
||||
section Test
|
||||
TextWithSpaces: 5: Gustavo Fring is played by Giancarlo Esposito and is a character in Breaking Bad.
|
||||
`,
|
||||
{ journey: { useMaxWidth: true } }
|
||||
);
|
||||
|
||||
// Verify that none of the text spans end with a hyphen.
|
||||
cy.get('tspan').each(($el) => {
|
||||
const text = $el.text();
|
||||
expect(text.trim()).not.to.match(/-$/);
|
||||
});
|
||||
});
|
||||
|
||||
it('should wrap long labels into multiple lines, keep them under max width, and maintain margins', () => {
|
||||
renderGraph(
|
||||
`
|
||||
---
|
||||
config:
|
||||
journey:
|
||||
maxLabelWidth: 320
|
||||
---
|
||||
journey
|
||||
title User Journey Example
|
||||
section Onboarding
|
||||
Sign Up: 5: This is a long label that will be split into multiple lines to test the wrapping functionality
|
||||
Browse Features: 3: This is another long label that will be split into multiple lines to test the wrapping functionality
|
||||
Use Core Functionality: 4: This is yet another long label that will be split into multiple lines to test the wrapping functionality
|
||||
section Engagement
|
||||
Browse Features: 3
|
||||
Use Core Functionality: 4
|
||||
`,
|
||||
{ journey: { useMaxWidth: true } }
|
||||
);
|
||||
|
||||
let diagramStartX, maxLineWidth;
|
||||
|
||||
// Get the diagram's left edge x-coordinate
|
||||
cy.contains('foreignobject', 'Sign Up')
|
||||
.then(($diagram) => {
|
||||
diagramStartX = parseFloat($diagram.attr('x'));
|
||||
})
|
||||
.then(() => {
|
||||
cy.get('text.legend').then(($lines) => {
|
||||
// Check that there are multiple lines
|
||||
expect($lines.length).to.be.equal(9);
|
||||
|
||||
// Check that all lines are under the maxLabelWidth
|
||||
$lines.each((index, el) => {
|
||||
const bbox = el.getBBox();
|
||||
expect(bbox.width).to.be.lte(320);
|
||||
maxLineWidth = Math.max(maxLineWidth || 0, bbox.width);
|
||||
});
|
||||
|
||||
/** The expected margin between the diagram and the legend is 150px, as defined by
|
||||
* conf.leftMargin in user-journey-config.js
|
||||
*/
|
||||
expect(diagramStartX - maxLineWidth).to.be.closeTo(150, 2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should correctly render the user journey diagram title with the specified styling', () => {
|
||||
renderGraph(
|
||||
`---
|
||||
config:
|
||||
journey:
|
||||
titleColor: "#2900A5"
|
||||
titleFontFamily: "Times New Roman"
|
||||
titleFontSize: "5rem"
|
||||
---
|
||||
|
||||
journey
|
||||
title User Journey Example
|
||||
section Onboarding
|
||||
Sign Up: 5: John, Shahir
|
||||
Complete Profile: 4: John
|
||||
section Engagement
|
||||
Browse Features: 3: John
|
||||
Use Core Functionality: 4: John
|
||||
section Retention
|
||||
Revisit Application: 5: John
|
||||
Invite Friends: 3: John
|
||||
|
||||
size: 2rem
|
||||
`
|
||||
);
|
||||
|
||||
cy.get('text').contains('User Journey Example').as('title');
|
||||
cy.get('@title').then(($title) => {
|
||||
expect($title).to.have.attr('fill', '#2900A5');
|
||||
expect($title).to.have.attr('font-family', 'Times New Roman');
|
||||
expect($title).to.have.attr('font-size', '5rem');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -179,6 +179,7 @@ describe('XY Chart', () => {
|
||||
axisLineWidth: 5
|
||||
chartOrientation: horizontal
|
||||
plotReservedSpacePercent: 60
|
||||
showDataLabel: true
|
||||
---
|
||||
xychart-beta
|
||||
title "Sales Revenue"
|
||||
@@ -315,4 +316,516 @@ describe('XY Chart', () => {
|
||||
);
|
||||
cy.get('svg');
|
||||
});
|
||||
|
||||
it('should render vertical bar chart with labels', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
---
|
||||
config:
|
||||
xyChart:
|
||||
showDataLabel: true
|
||||
---
|
||||
xychart-beta
|
||||
title "Sales Revenue"
|
||||
x-axis Months [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec]
|
||||
y-axis "Revenue (in $)" 4000 --> 11000
|
||||
bar [5000, 6000, 7500, 8200, 9500, 10500, 11000, 10200, 9200, 8500, 7000, 6000]
|
||||
`,
|
||||
{}
|
||||
);
|
||||
});
|
||||
|
||||
it('should render horizontal bar chart with labels', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
---
|
||||
config:
|
||||
xyChart:
|
||||
showDataLabel: true
|
||||
chartOrientation: horizontal
|
||||
---
|
||||
xychart-beta
|
||||
title "Sales Revenue"
|
||||
x-axis Months [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec]
|
||||
y-axis "Revenue (in $)" 4000 --> 11000
|
||||
bar [5000, 6000, 7500, 8200, 9500, 10500, 11000, 10200, 9200, 8500, 7000, 6000]
|
||||
`,
|
||||
{}
|
||||
);
|
||||
});
|
||||
|
||||
it('should render vertical bar chart without labels by default', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
xychart-beta
|
||||
title "Sales Revenue"
|
||||
x-axis Months [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec]
|
||||
y-axis "Revenue (in $)" 4000 --> 11000
|
||||
bar [5000, 6000, 7500, 8200, 9500, 10500, 11000, 10200, 9200, 8500, 7000, 6000]
|
||||
`,
|
||||
{}
|
||||
);
|
||||
});
|
||||
|
||||
it('should render horizontal bar chart without labels by default', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
---
|
||||
config:
|
||||
xyChart:
|
||||
chartOrientation: horizontal
|
||||
---
|
||||
xychart-beta
|
||||
title "Sales Revenue"
|
||||
x-axis Months [jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec]
|
||||
y-axis "Revenue (in $)" 4000 --> 11000
|
||||
bar [5000, 6000, 7500, 8200, 9500, 10500, 11000, 10200, 9200, 8500, 7000, 6000]
|
||||
`,
|
||||
{}
|
||||
);
|
||||
});
|
||||
|
||||
it('should render multiple bar plots vertically with labels correctly', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
---
|
||||
config:
|
||||
xyChart:
|
||||
showDataLabel: true
|
||||
---
|
||||
xychart-beta
|
||||
title "Multiple Bar Plots"
|
||||
x-axis Categories [A, B, C]
|
||||
y-axis "Values" 0 --> 100
|
||||
bar [10, 50, 90]
|
||||
`,
|
||||
{}
|
||||
);
|
||||
});
|
||||
|
||||
it('should render multiple bar plots horizontally with labels correctly', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
---
|
||||
config:
|
||||
xyChart:
|
||||
showDataLabel: true
|
||||
chartOrientation: horizontal
|
||||
---
|
||||
xychart-beta
|
||||
title "Multiple Bar Plots"
|
||||
x-axis Categories [A, B, C]
|
||||
y-axis "Values" 0 --> 100
|
||||
bar [10, 50, 90]
|
||||
`,
|
||||
{}
|
||||
);
|
||||
});
|
||||
|
||||
it('should render a single bar with label for a vertical xy-chart', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
---
|
||||
config:
|
||||
xyChart:
|
||||
showDataLabel: true
|
||||
---
|
||||
xychart-beta
|
||||
title "Single Bar Chart"
|
||||
x-axis Categories [A]
|
||||
y-axis "Value" 0 --> 100
|
||||
bar [75]
|
||||
`,
|
||||
{}
|
||||
);
|
||||
});
|
||||
|
||||
it('should render a single bar with label for a horizontal xy-chart', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
---
|
||||
config:
|
||||
xyChart:
|
||||
showDataLabel: true
|
||||
chartOrientation: horizontal
|
||||
---
|
||||
xychart-beta
|
||||
title "Single Bar Chart"
|
||||
x-axis Categories [A]
|
||||
y-axis "Value" 0 --> 100
|
||||
bar [75]
|
||||
`,
|
||||
{}
|
||||
);
|
||||
});
|
||||
|
||||
it('should render negative and decimal values with correct labels for vertical xy-chart', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
---
|
||||
config:
|
||||
xyChart:
|
||||
showDataLabel: true
|
||||
---
|
||||
xychart-beta
|
||||
title "Decimal and Negative Values"
|
||||
x-axis Categories [A, B, C]
|
||||
y-axis -10 --> 10
|
||||
bar [ -2.5, 0.75, 5.1 ]
|
||||
`,
|
||||
{}
|
||||
);
|
||||
});
|
||||
|
||||
it('should render negative and decimal values with correct labels for horizontal xy-chart', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
---
|
||||
config:
|
||||
xyChart:
|
||||
showDataLabel: true
|
||||
chartOrientation: horizontal
|
||||
---
|
||||
xychart-beta
|
||||
title "Decimal and Negative Values"
|
||||
x-axis Categories [A, B, C]
|
||||
y-axis -10 --> 10
|
||||
bar [ -2.5, 0.75, 5.1 ]
|
||||
`,
|
||||
{}
|
||||
);
|
||||
});
|
||||
|
||||
it('should render data labels within each bar in the vertical xy-chart', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
---
|
||||
config:
|
||||
xyChart:
|
||||
showDataLabel: true
|
||||
---
|
||||
xychart-beta
|
||||
title "Sales Revenue"
|
||||
x-axis Months [jan,b,c]
|
||||
y-axis "Revenue (in $)" 4000 --> 12000
|
||||
bar [5000, 6000, 7500, 8200, 9500, 10500, 11000, 10200, 9200, 8500, 7000, 6000, 3000, 2000, 500, 2000, 3000, 11000, 5000, 6000]
|
||||
`,
|
||||
{}
|
||||
);
|
||||
|
||||
cy.get('g.bar-plot-0').within(() => {
|
||||
cy.get('rect').each(($rect, index) => {
|
||||
// Extract bar properties
|
||||
const barProps = {
|
||||
x: parseFloat($rect.attr('x')),
|
||||
y: parseFloat($rect.attr('y')),
|
||||
width: parseFloat($rect.attr('width')),
|
||||
height: parseFloat($rect.attr('height')),
|
||||
};
|
||||
|
||||
// Get the text element corresponding to this bar by index.
|
||||
cy.get('text')
|
||||
.eq(index)
|
||||
.then(($text) => {
|
||||
const bbox = $text[0].getBBox();
|
||||
const textProps = {
|
||||
x: bbox.x,
|
||||
y: bbox.y,
|
||||
width: bbox.width,
|
||||
height: bbox.height,
|
||||
};
|
||||
|
||||
// Verify that the text label is positioned within the boundaries of the bar.
|
||||
expect(textProps.x).to.be.greaterThan(barProps.x);
|
||||
expect(textProps.x + textProps.width).to.be.lessThan(barProps.x + barProps.width);
|
||||
|
||||
// Check horizontal alignment (within tolerance)
|
||||
expect(textProps.x + textProps.width / 2).to.be.closeTo(
|
||||
barProps.x + barProps.width / 2,
|
||||
5
|
||||
);
|
||||
|
||||
expect(textProps.y).to.be.greaterThan(barProps.y);
|
||||
expect(textProps.y + textProps.height).to.be.lessThan(barProps.y + barProps.height);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should render data labels within each bar in the horizontal xy-chart', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
---
|
||||
config:
|
||||
xyChart:
|
||||
showDataLabel: true
|
||||
chartOrientation: horizontal
|
||||
---
|
||||
xychart-beta
|
||||
title "Sales Revenue"
|
||||
x-axis Months [jan,b,c]
|
||||
y-axis "Revenue (in $)" 4000 --> 12000
|
||||
bar [5000, 6000, 7500, 8200, 9500, 10500, 11000, 10200, 9200, 8500, 7000, 6000, 3000, 2000, 500, 2000, 3000, 11000, 5000, 6000]
|
||||
`,
|
||||
{}
|
||||
);
|
||||
|
||||
cy.get('g.bar-plot-0').within(() => {
|
||||
cy.get('rect').each(($rect, index) => {
|
||||
// Extract bar properties
|
||||
const barProps = {
|
||||
x: parseFloat($rect.attr('x')),
|
||||
y: parseFloat($rect.attr('y')),
|
||||
width: parseFloat($rect.attr('width')),
|
||||
height: parseFloat($rect.attr('height')),
|
||||
};
|
||||
|
||||
// Get the text element corresponding to this bar by index.
|
||||
cy.get('text')
|
||||
.eq(index)
|
||||
.then(($text) => {
|
||||
const bbox = $text[0].getBBox();
|
||||
const textProps = {
|
||||
x: bbox.x,
|
||||
y: bbox.y,
|
||||
width: bbox.width,
|
||||
height: bbox.height,
|
||||
};
|
||||
|
||||
// Verify that the text label is positioned within the boundaries of the bar.
|
||||
expect(textProps.x).to.be.greaterThan(barProps.x);
|
||||
expect(textProps.x + textProps.width).to.be.lessThan(barProps.x + barProps.width);
|
||||
|
||||
expect(textProps.y).to.be.greaterThan(barProps.y);
|
||||
expect(textProps.y + textProps.height).to.be.lessThan(barProps.y + barProps.height);
|
||||
expect(textProps.y + textProps.height / 2).to.be.closeTo(
|
||||
barProps.y + barProps.height / 2,
|
||||
5
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should render data labels within each bar in the vertical xy-chart with a lot of bars of different sizes', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
---
|
||||
config:
|
||||
xyChart:
|
||||
showDataLabel: true
|
||||
---
|
||||
xychart-beta
|
||||
title "Sales Revenue"
|
||||
x-axis Months [jan,a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s]
|
||||
y-axis "Revenue (in $)" 4000 --> 12000
|
||||
bar [5000, 6000, 7500, 8200, 9500, 10500, 11000, 10200, 9200, 8500, 7000, 6000, 8000, 10000, 5000, 7600, 4999,11000 ,5000,6000]
|
||||
`,
|
||||
{}
|
||||
);
|
||||
|
||||
cy.get('g.bar-plot-0').within(() => {
|
||||
cy.get('rect').each(($rect, index) => {
|
||||
// Extract bar properties
|
||||
const barProps = {
|
||||
x: parseFloat($rect.attr('x')),
|
||||
y: parseFloat($rect.attr('y')),
|
||||
width: parseFloat($rect.attr('width')),
|
||||
height: parseFloat($rect.attr('height')),
|
||||
};
|
||||
|
||||
// Get the text element corresponding to this bar by index.
|
||||
cy.get('text')
|
||||
.eq(index)
|
||||
.then(($text) => {
|
||||
const bbox = $text[0].getBBox();
|
||||
const textProps = {
|
||||
x: bbox.x,
|
||||
y: bbox.y,
|
||||
width: bbox.width,
|
||||
height: bbox.height,
|
||||
};
|
||||
|
||||
// Verify that the text label is positioned within the boundaries of the bar.
|
||||
expect(textProps.x).to.be.greaterThan(barProps.x);
|
||||
expect(textProps.x + textProps.width).to.be.lessThan(barProps.x + barProps.width);
|
||||
|
||||
// Check horizontal alignment (within tolerance)
|
||||
expect(textProps.x + textProps.width / 2).to.be.closeTo(
|
||||
barProps.x + barProps.width / 2,
|
||||
5
|
||||
);
|
||||
|
||||
expect(textProps.y).to.be.greaterThan(barProps.y);
|
||||
expect(textProps.y + textProps.height).to.be.lessThan(barProps.y + barProps.height);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should render data labels within each bar in the horizontal xy-chart with a lot of bars of different sizes', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
---
|
||||
config:
|
||||
xyChart:
|
||||
showDataLabel: true
|
||||
chartOrientation: horizontal
|
||||
---
|
||||
xychart-beta
|
||||
title "Sales Revenue"
|
||||
x-axis Months [jan,a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s]
|
||||
y-axis "Revenue (in $)" 4000 --> 12000
|
||||
bar [5000, 6000, 7500, 8200, 9500, 10500, 11000, 10200, 9200, 8500, 7000, 6000, 8000, 10000, 5000, 7600, 4999,11000 ,5000,6000]
|
||||
`,
|
||||
{}
|
||||
);
|
||||
|
||||
cy.get('g.bar-plot-0').within(() => {
|
||||
cy.get('rect').each(($rect, index) => {
|
||||
// Extract bar properties
|
||||
const barProps = {
|
||||
x: parseFloat($rect.attr('x')),
|
||||
y: parseFloat($rect.attr('y')),
|
||||
width: parseFloat($rect.attr('width')),
|
||||
height: parseFloat($rect.attr('height')),
|
||||
};
|
||||
|
||||
// Get the text element corresponding to this bar by index.
|
||||
cy.get('text')
|
||||
.eq(index)
|
||||
.then(($text) => {
|
||||
const bbox = $text[0].getBBox();
|
||||
const textProps = {
|
||||
x: bbox.x,
|
||||
y: bbox.y,
|
||||
width: bbox.width,
|
||||
height: bbox.height,
|
||||
};
|
||||
|
||||
// Verify that the text label is positioned within the boundaries of the bar.
|
||||
expect(textProps.x).to.be.greaterThan(barProps.x);
|
||||
expect(textProps.x + textProps.width).to.be.lessThan(barProps.x + barProps.width);
|
||||
|
||||
expect(textProps.y).to.be.greaterThan(barProps.y);
|
||||
expect(textProps.y + textProps.height).to.be.lessThan(barProps.y + barProps.height);
|
||||
expect(textProps.y + textProps.height / 2).to.be.closeTo(
|
||||
barProps.y + barProps.height / 2,
|
||||
5
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should render data labels correctly for a bar in the vertical xy-chart', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
---
|
||||
config:
|
||||
xyChart:
|
||||
showDataLabel: true
|
||||
---
|
||||
xychart-beta
|
||||
title "Sales Revenue"
|
||||
x-axis Months [jan]
|
||||
y-axis "Revenue (in $)" 3000 --> 12000
|
||||
bar [4000]
|
||||
`,
|
||||
{}
|
||||
);
|
||||
|
||||
cy.get('g.bar-plot-0').within(() => {
|
||||
cy.get('rect').each(($rect, index) => {
|
||||
// Extract bar properties
|
||||
const barProps = {
|
||||
x: parseFloat($rect.attr('x')),
|
||||
y: parseFloat($rect.attr('y')),
|
||||
width: parseFloat($rect.attr('width')),
|
||||
height: parseFloat($rect.attr('height')),
|
||||
};
|
||||
|
||||
// Get the text element corresponding to this bar by index.
|
||||
cy.get('text')
|
||||
.eq(index)
|
||||
.then(($text) => {
|
||||
const bbox = $text[0].getBBox();
|
||||
const textProps = {
|
||||
x: bbox.x,
|
||||
y: bbox.y,
|
||||
width: bbox.width,
|
||||
height: bbox.height,
|
||||
};
|
||||
|
||||
// Verify that the text label is positioned within the boundaries of the bar.
|
||||
expect(textProps.x).to.be.greaterThan(barProps.x);
|
||||
expect(textProps.x + textProps.width).to.be.lessThan(barProps.x + barProps.width);
|
||||
|
||||
// Check horizontal alignment (within tolerance)
|
||||
expect(textProps.x + textProps.width / 2).to.be.closeTo(
|
||||
barProps.x + barProps.width / 2,
|
||||
5
|
||||
);
|
||||
|
||||
expect(textProps.y).to.be.greaterThan(barProps.y);
|
||||
expect(textProps.y + textProps.height).to.be.lessThan(barProps.y + barProps.height);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should render data labels correctly for a bar in the horizontal xy-chart', () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
---
|
||||
config:
|
||||
xyChart:
|
||||
showDataLabel: true
|
||||
chartOrientation: horizontal
|
||||
---
|
||||
xychart-beta
|
||||
title "Sales Revenue"
|
||||
x-axis Months [jan]
|
||||
y-axis "Revenue (in $)" 3000 --> 12000
|
||||
bar [4000]
|
||||
`,
|
||||
{}
|
||||
);
|
||||
|
||||
cy.get('g.bar-plot-0').within(() => {
|
||||
cy.get('rect').each(($rect, index) => {
|
||||
// Extract bar properties
|
||||
const barProps = {
|
||||
x: parseFloat($rect.attr('x')),
|
||||
y: parseFloat($rect.attr('y')),
|
||||
width: parseFloat($rect.attr('width')),
|
||||
height: parseFloat($rect.attr('height')),
|
||||
};
|
||||
|
||||
// Get the text element corresponding to this bar by index.
|
||||
cy.get('text')
|
||||
.eq(index)
|
||||
.then(($text) => {
|
||||
const bbox = $text[0].getBBox();
|
||||
const textProps = {
|
||||
x: bbox.x,
|
||||
y: bbox.y,
|
||||
width: bbox.width,
|
||||
height: bbox.height,
|
||||
};
|
||||
|
||||
// Verify that the text label is positioned within the boundaries of the bar.
|
||||
expect(textProps.x).to.be.greaterThan(barProps.x);
|
||||
expect(textProps.x + textProps.width).to.be.lessThan(barProps.x + barProps.width);
|
||||
|
||||
expect(textProps.y).to.be.greaterThan(barProps.y);
|
||||
expect(textProps.y + textProps.height).to.be.lessThan(barProps.y + barProps.height);
|
||||
expect(textProps.y + textProps.height / 2).to.be.closeTo(
|
||||
barProps.y + barProps.height / 2,
|
||||
5
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,211 +2,211 @@
|
||||
"durations": [
|
||||
{
|
||||
"spec": "cypress/integration/other/configuration.spec.js",
|
||||
"duration": 5475
|
||||
"duration": 5450
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/other/external-diagrams.spec.js",
|
||||
"duration": 2037
|
||||
"duration": 2004
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/other/ghsa.spec.js",
|
||||
"duration": 3207
|
||||
"duration": 3183
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/other/iife.spec.js",
|
||||
"duration": 1915
|
||||
"duration": 1913
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/other/interaction.spec.js",
|
||||
"duration": 10952
|
||||
"duration": 10944
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/other/rerender.spec.js",
|
||||
"duration": 1872
|
||||
"duration": 1938
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/other/xss.spec.js",
|
||||
"duration": 26686
|
||||
"duration": 26753
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/appli.spec.js",
|
||||
"duration": 2629
|
||||
"duration": 2571
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/architecture.spec.ts",
|
||||
"duration": 104
|
||||
"duration": 110
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/block.spec.js",
|
||||
"duration": 14765
|
||||
"duration": 14697
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/c4.spec.js",
|
||||
"duration": 4913
|
||||
"duration": 4705
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/classDiagram-elk-v3.spec.js",
|
||||
"duration": 36667
|
||||
"duration": 35841
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/classDiagram-handDrawn-v3.spec.js",
|
||||
"duration": 33813
|
||||
"duration": 34279
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/classDiagram-v2.spec.js",
|
||||
"duration": 20441
|
||||
"duration": 20641
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/classDiagram-v3.spec.js",
|
||||
"duration": 32504
|
||||
"duration": 33020
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/classDiagram.spec.js",
|
||||
"duration": 13772
|
||||
"duration": 13546
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/conf-and-directives.spec.js",
|
||||
"duration": 7978
|
||||
"duration": 8072
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/current.spec.js",
|
||||
"duration": 2101
|
||||
"duration": 2083
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/erDiagram-unified.spec.js",
|
||||
"duration": 76556
|
||||
"duration": 78269
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/erDiagram.spec.js",
|
||||
"duration": 12756
|
||||
"duration": 12578
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/errorDiagram.spec.js",
|
||||
"duration": 2766
|
||||
"duration": 2784
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/flowchart-elk.spec.js",
|
||||
"duration": 35641
|
||||
"duration": 36205
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/flowchart-handDrawn.spec.js",
|
||||
"duration": 26915
|
||||
"duration": 26627
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/flowchart-shape-alias.spec.ts",
|
||||
"duration": 21171
|
||||
"duration": 21332
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/flowchart-v2.spec.js",
|
||||
"duration": 37844
|
||||
"duration": 37328
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/flowchart.spec.js",
|
||||
"duration": 26254
|
||||
"duration": 25914
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/gantt.spec.js",
|
||||
"duration": 15149
|
||||
"duration": 15383
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/gitGraph.spec.js",
|
||||
"duration": 45049
|
||||
"duration": 45226
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/iconShape.spec.ts",
|
||||
"duration": 250225
|
||||
"duration": 251094
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/imageShape.spec.ts",
|
||||
"duration": 51531
|
||||
"duration": 50916
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/info.spec.ts",
|
||||
"duration": 2455
|
||||
"duration": 2489
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/journey.spec.js",
|
||||
"duration": 3181
|
||||
"duration": 5988
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/kanban.spec.ts",
|
||||
"duration": 6298
|
||||
"duration": 6225
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/katex.spec.js",
|
||||
"duration": 3065
|
||||
"duration": 3009
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/marker_unique_id.spec.js",
|
||||
"duration": 2521
|
||||
"duration": 2426
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/mindmap.spec.ts",
|
||||
"duration": 9341
|
||||
"duration": 9306
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/newShapes.spec.ts",
|
||||
"duration": 132809
|
||||
"duration": 134419
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/oldShapes.spec.ts",
|
||||
"duration": 101299
|
||||
"duration": 102434
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/packet.spec.ts",
|
||||
"duration": 3481
|
||||
"duration": 3373
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/pie.spec.ts",
|
||||
"duration": 4878
|
||||
"duration": 4898
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/quadrantChart.spec.js",
|
||||
"duration": 7416
|
||||
"duration": 7578
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/radar.spec.js",
|
||||
"duration": 4554
|
||||
"duration": 4526
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/requirement.spec.js",
|
||||
"duration": 2068
|
||||
"duration": 2172
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/requirementDiagram-unified.spec.js",
|
||||
"duration": 47583
|
||||
"duration": 47175
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/sankey.spec.ts",
|
||||
"duration": 5792
|
||||
"duration": 5717
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/sequencediagram.spec.js",
|
||||
"duration": 33035
|
||||
"duration": 32556
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/stateDiagram-v2.spec.js",
|
||||
"duration": 22716
|
||||
"duration": 22572
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/stateDiagram.spec.js",
|
||||
"duration": 13868
|
||||
"duration": 14064
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/theme.spec.js",
|
||||
"duration": 26376
|
||||
"duration": 26565
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/timeline.spec.ts",
|
||||
"duration": 5872
|
||||
"duration": 6233
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/xyChart.spec.js",
|
||||
"duration": 9469
|
||||
"duration": 17750
|
||||
},
|
||||
{
|
||||
"spec": "cypress/integration/rendering/zenuml.spec.js",
|
||||
"duration": 2742
|
||||
"duration": 2696
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -53,18 +53,6 @@ Example of `init` directive setting the `theme` to `forest`:
|
||||
a --> b
|
||||
```
|
||||
|
||||
```mermaid-example
|
||||
%%{init: {'theme':'forest'}}%%
|
||||
graph TD
|
||||
a --> b
|
||||
```
|
||||
|
||||
```mermaid
|
||||
%%{init: {'theme':'forest'}}%%
|
||||
graph TD
|
||||
a --> b
|
||||
```
|
||||
|
||||
> **Reminder**: the only theme that can be customized is the `base` theme. The following section covers how to use `themeVariables` for customizations.
|
||||
|
||||
## Customizing Themes with `themeVariables`
|
||||
@@ -139,66 +127,6 @@ Example of modifying `themeVariables` using the `init` directive:
|
||||
end
|
||||
```
|
||||
|
||||
```mermaid-example
|
||||
%%{
|
||||
init: {
|
||||
'theme': 'base',
|
||||
'themeVariables': {
|
||||
'primaryColor': '#BB2528',
|
||||
'primaryTextColor': '#fff',
|
||||
'primaryBorderColor': '#7C0000',
|
||||
'lineColor': '#F8B229',
|
||||
'secondaryColor': '#006100',
|
||||
'tertiaryColor': '#fff'
|
||||
}
|
||||
}
|
||||
}%%
|
||||
graph TD
|
||||
A[Christmas] -->|Get money| B(Go shopping)
|
||||
B --> C{Let me think}
|
||||
B --> G[/Another/]
|
||||
C ==>|One| D[Laptop]
|
||||
C -->|Two| E[iPhone]
|
||||
C -->|Three| F[fa:fa-car Car]
|
||||
subgraph section
|
||||
C
|
||||
D
|
||||
E
|
||||
F
|
||||
G
|
||||
end
|
||||
```
|
||||
|
||||
```mermaid
|
||||
%%{
|
||||
init: {
|
||||
'theme': 'base',
|
||||
'themeVariables': {
|
||||
'primaryColor': '#BB2528',
|
||||
'primaryTextColor': '#fff',
|
||||
'primaryBorderColor': '#7C0000',
|
||||
'lineColor': '#F8B229',
|
||||
'secondaryColor': '#006100',
|
||||
'tertiaryColor': '#fff'
|
||||
}
|
||||
}
|
||||
}%%
|
||||
graph TD
|
||||
A[Christmas] -->|Get money| B(Go shopping)
|
||||
B --> C{Let me think}
|
||||
B --> G[/Another/]
|
||||
C ==>|One| D[Laptop]
|
||||
C -->|Two| E[iPhone]
|
||||
C -->|Three| F[fa:fa-car Car]
|
||||
subgraph section
|
||||
C
|
||||
D
|
||||
E
|
||||
F
|
||||
G
|
||||
end
|
||||
```
|
||||
|
||||
## Color and Color Calculation
|
||||
|
||||
To ensure diagram readability, the default value of certain variables is calculated or derived from other variables. For example, `primaryBorderColor` is derived from the `primaryColor` variable. So if the `primaryColor` variable is customized, Mermaid will adjust `primaryBorderColor` automatically. Adjustments can mean a color inversion, a hue change, a darkening/lightening by 10%, etc.
|
||||
|
||||
@@ -40,6 +40,7 @@ To add an integration to this list, see the [Integrations - create page](./integ
|
||||
- [Mermaid Charts & Diagrams for Jira](https://marketplace.atlassian.com/apps/1224537/)
|
||||
- [Mermaid for Jira Cloud - Draw UML diagrams easily](https://marketplace.atlassian.com/apps/1223053/mermaid-for-jira-cloud-draw-uml-diagrams-easily?hosting=cloud&tab=overview)
|
||||
- [CloudScript.io Mermaid Addon](https://marketplace.atlassian.com/apps/1219878/cloudscript-io-mermaid-addon?hosting=cloud&tab=overview)
|
||||
- [Mermaid plus for Confluence](https://marketplace.atlassian.com/apps/1236814/mermaid-plus-for-confluence?hosting=cloud&tab=overview)
|
||||
- [Azure Devops](https://learn.microsoft.com/en-us/azure/devops/project/wiki/markdown-guidance?view=azure-devops#add-mermaid-diagrams-to-a-wiki-page) ✅
|
||||
- [Deepdwn](https://billiam.itch.io/deepdwn) ✅
|
||||
- [Doctave](https://www.doctave.com/) ✅
|
||||
@@ -267,7 +268,5 @@ Communication tools and platforms
|
||||
- [reveal.js-mermaid-plugin](https://github.com/ludwick/reveal.js-mermaid-plugin)
|
||||
- [Reveal CK](https://github.com/jedcn/reveal-ck)
|
||||
- [reveal-ck-mermaid-plugin](https://github.com/tmtm/reveal-ck-mermaid-plugin)
|
||||
- [mermaid-isomorphic](https://github.com/remcohaszing/mermaid-isomorphic)
|
||||
- [mermaid-server: Generate diagrams using a HTTP request](https://github.com/TomWright/mermaid-server)
|
||||
|
||||
<!--- cspell:ignore Blazorade HueHive --->
|
||||
|
||||
@@ -1193,12 +1193,12 @@ To give an edge an ID, prepend the edge syntax with the ID followed by an `@` ch
|
||||
|
||||
```mermaid-example
|
||||
flowchart LR
|
||||
A e1@–> B
|
||||
A e1@--> B
|
||||
```
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A e1@–> B
|
||||
A e1@--> B
|
||||
```
|
||||
|
||||
In this example, `e1` is the ID of the edge connecting `A` to `B`. You can then use this ID in later definitions or style statements, just like with nodes.
|
||||
@@ -1229,13 +1229,13 @@ In the initial version, two animation speeds are supported: `fast` and `slow`. S
|
||||
|
||||
```mermaid-example
|
||||
flowchart LR
|
||||
A e1@–> B
|
||||
A e1@--> B
|
||||
e1@{ animation: fast }
|
||||
```
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A e1@–> B
|
||||
A e1@--> B
|
||||
e1@{ animation: fast }
|
||||
```
|
||||
|
||||
@@ -1247,14 +1247,14 @@ You can also animate edges by assigning a class to them and then defining animat
|
||||
|
||||
```mermaid-example
|
||||
flowchart LR
|
||||
A e1@–> B
|
||||
A e1@--> B
|
||||
classDef animate stroke-dasharray: 9,5,stroke-dashoffset: 900,animation: dash 25s linear infinite;
|
||||
class e1 animate
|
||||
```
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A e1@–> B
|
||||
A e1@--> B
|
||||
classDef animate stroke-dasharray: 9,5,stroke-dashoffset: 900,animation: dash 25s linear infinite;
|
||||
class e1 animate
|
||||
```
|
||||
|
||||
@@ -432,7 +432,13 @@ Sometimes you may want to hide the branch names and lines from the diagram. You
|
||||
Usage example:
|
||||
|
||||
```mermaid-example
|
||||
%%{init: { 'logLevel': 'debug', 'theme': 'base', 'gitGraph': {'showBranches': false}} }%%
|
||||
---
|
||||
config:
|
||||
logLevel: 'debug'
|
||||
theme: 'base'
|
||||
gitGraph:
|
||||
showBranches: false
|
||||
---
|
||||
gitGraph
|
||||
commit
|
||||
branch hotfix
|
||||
@@ -478,7 +484,13 @@ Usage example:
|
||||
```
|
||||
|
||||
```mermaid
|
||||
%%{init: { 'logLevel': 'debug', 'theme': 'base', 'gitGraph': {'showBranches': false}} }%%
|
||||
---
|
||||
config:
|
||||
logLevel: 'debug'
|
||||
theme: 'base'
|
||||
gitGraph:
|
||||
showBranches: false
|
||||
---
|
||||
gitGraph
|
||||
commit
|
||||
branch hotfix
|
||||
@@ -534,7 +546,13 @@ You can change the layout of the commit labels by using the `rotateCommitLabel`
|
||||
Usage example: Rotated commit labels
|
||||
|
||||
```mermaid-example
|
||||
%%{init: { 'logLevel': 'debug', 'theme': 'base', 'gitGraph': {'rotateCommitLabel': true}} }%%
|
||||
---
|
||||
config:
|
||||
logLevel: 'debug'
|
||||
theme: 'base'
|
||||
gitGraph:
|
||||
rotateCommitLabel: true
|
||||
---
|
||||
gitGraph
|
||||
commit id: "feat(api): ..."
|
||||
commit id: "a"
|
||||
@@ -553,7 +571,13 @@ gitGraph
|
||||
```
|
||||
|
||||
```mermaid
|
||||
%%{init: { 'logLevel': 'debug', 'theme': 'base', 'gitGraph': {'rotateCommitLabel': true}} }%%
|
||||
---
|
||||
config:
|
||||
logLevel: 'debug'
|
||||
theme: 'base'
|
||||
gitGraph:
|
||||
rotateCommitLabel: true
|
||||
---
|
||||
gitGraph
|
||||
commit id: "feat(api): ..."
|
||||
commit id: "a"
|
||||
@@ -574,7 +598,13 @@ gitGraph
|
||||
Usage example: Horizontal commit labels
|
||||
|
||||
```mermaid-example
|
||||
%%{init: { 'logLevel': 'debug', 'theme': 'base', 'gitGraph': {'rotateCommitLabel': false}} }%%
|
||||
---
|
||||
config:
|
||||
logLevel: 'debug'
|
||||
theme: 'base'
|
||||
gitGraph:
|
||||
rotateCommitLabel: false
|
||||
---
|
||||
gitGraph
|
||||
commit id: "feat(api): ..."
|
||||
commit id: "a"
|
||||
@@ -593,7 +623,13 @@ gitGraph
|
||||
```
|
||||
|
||||
```mermaid
|
||||
%%{init: { 'logLevel': 'debug', 'theme': 'base', 'gitGraph': {'rotateCommitLabel': false}} }%%
|
||||
---
|
||||
config:
|
||||
logLevel: 'debug'
|
||||
theme: 'base'
|
||||
gitGraph:
|
||||
rotateCommitLabel: false
|
||||
---
|
||||
gitGraph
|
||||
commit id: "feat(api): ..."
|
||||
commit id: "a"
|
||||
@@ -618,7 +654,14 @@ Sometimes you may want to hide the commit labels from the diagram. You can do th
|
||||
Usage example:
|
||||
|
||||
```mermaid-example
|
||||
%%{init: { 'logLevel': 'debug', 'theme': 'base', 'gitGraph': {'showBranches': false,'showCommitLabel': false}} }%%
|
||||
---
|
||||
config:
|
||||
logLevel: 'debug'
|
||||
theme: 'base'
|
||||
gitGraph:
|
||||
showBranches: false
|
||||
showCommitLabel: false
|
||||
---
|
||||
gitGraph
|
||||
commit
|
||||
branch hotfix
|
||||
@@ -664,7 +707,14 @@ Usage example:
|
||||
```
|
||||
|
||||
```mermaid
|
||||
%%{init: { 'logLevel': 'debug', 'theme': 'base', 'gitGraph': {'showBranches': false,'showCommitLabel': false}} }%%
|
||||
---
|
||||
config:
|
||||
logLevel: 'debug'
|
||||
theme: 'base'
|
||||
gitGraph:
|
||||
showBranches: false
|
||||
showCommitLabel: false
|
||||
---
|
||||
gitGraph
|
||||
commit
|
||||
branch hotfix
|
||||
@@ -716,7 +766,15 @@ Sometimes you may want to customize the name of the main/default branch. You can
|
||||
Usage example:
|
||||
|
||||
```mermaid-example
|
||||
%%{init: { 'logLevel': 'debug', 'theme': 'base', 'gitGraph': {'showBranches': true, 'showCommitLabel':true,'mainBranchName': 'MetroLine1'}} }%%
|
||||
---
|
||||
config:
|
||||
logLevel: 'debug'
|
||||
theme: 'base'
|
||||
gitGraph:
|
||||
showBranches: true
|
||||
showCommitLabel: true
|
||||
mainBranchName: 'MetroLine1'
|
||||
---
|
||||
gitGraph
|
||||
commit id:"NewYork"
|
||||
commit id:"Dallas"
|
||||
@@ -740,7 +798,15 @@ Usage example:
|
||||
```
|
||||
|
||||
```mermaid
|
||||
%%{init: { 'logLevel': 'debug', 'theme': 'base', 'gitGraph': {'showBranches': true, 'showCommitLabel':true,'mainBranchName': 'MetroLine1'}} }%%
|
||||
---
|
||||
config:
|
||||
logLevel: 'debug'
|
||||
theme: 'base'
|
||||
gitGraph:
|
||||
showBranches: true
|
||||
showCommitLabel: true
|
||||
mainBranchName: 'MetroLine1'
|
||||
---
|
||||
gitGraph
|
||||
commit id:"NewYork"
|
||||
commit id:"Dallas"
|
||||
@@ -782,7 +848,14 @@ To fully control the order of all the branches, you must define `order` for all
|
||||
Usage example:
|
||||
|
||||
```mermaid-example
|
||||
%%{init: { 'logLevel': 'debug', 'theme': 'base', 'gitGraph': {'showBranches': true, 'showCommitLabel':true}} }%%
|
||||
---
|
||||
config:
|
||||
logLevel: 'debug'
|
||||
theme: 'base'
|
||||
gitGraph:
|
||||
showBranches: true
|
||||
showCommitLabel: true
|
||||
---
|
||||
gitGraph
|
||||
commit
|
||||
branch test1 order: 3
|
||||
@@ -792,7 +865,14 @@ Usage example:
|
||||
```
|
||||
|
||||
```mermaid
|
||||
%%{init: { 'logLevel': 'debug', 'theme': 'base', 'gitGraph': {'showBranches': true, 'showCommitLabel':true}} }%%
|
||||
---
|
||||
config:
|
||||
logLevel: 'debug'
|
||||
theme: 'base'
|
||||
gitGraph:
|
||||
showBranches: true
|
||||
showCommitLabel: true
|
||||
---
|
||||
gitGraph
|
||||
commit
|
||||
branch test1 order: 3
|
||||
@@ -806,7 +886,15 @@ Look at the diagram, all the branches are following the order defined.
|
||||
Usage example:
|
||||
|
||||
```mermaid-example
|
||||
%%{init: { 'logLevel': 'debug', 'theme': 'base', 'gitGraph': {'showBranches': true, 'showCommitLabel':true,'mainBranchOrder': 2}} }%%
|
||||
---
|
||||
config:
|
||||
logLevel: 'debug'
|
||||
theme: 'base'
|
||||
gitGraph:
|
||||
showBranches: true
|
||||
showCommitLabel: true
|
||||
mainBranchOrder: 2
|
||||
---
|
||||
gitGraph
|
||||
commit
|
||||
branch test1 order: 3
|
||||
@@ -817,7 +905,15 @@ Usage example:
|
||||
```
|
||||
|
||||
```mermaid
|
||||
%%{init: { 'logLevel': 'debug', 'theme': 'base', 'gitGraph': {'showBranches': true, 'showCommitLabel':true,'mainBranchOrder': 2}} }%%
|
||||
---
|
||||
config:
|
||||
logLevel: 'debug'
|
||||
theme: 'base'
|
||||
gitGraph:
|
||||
showBranches: true
|
||||
showCommitLabel: true
|
||||
mainBranchOrder: 2
|
||||
---
|
||||
gitGraph
|
||||
commit
|
||||
branch test1 order: 3
|
||||
@@ -1046,7 +1142,11 @@ Let's put them to use, and see how our sample diagram looks in different themes:
|
||||
### Base Theme
|
||||
|
||||
```mermaid-example
|
||||
%%{init: { 'logLevel': 'debug', 'theme': 'base' } }%%
|
||||
---
|
||||
config:
|
||||
logLevel: 'debug'
|
||||
theme: 'base'
|
||||
---
|
||||
gitGraph
|
||||
commit
|
||||
branch hotfix
|
||||
@@ -1092,7 +1192,11 @@ Let's put them to use, and see how our sample diagram looks in different themes:
|
||||
```
|
||||
|
||||
```mermaid
|
||||
%%{init: { 'logLevel': 'debug', 'theme': 'base' } }%%
|
||||
---
|
||||
config:
|
||||
logLevel: 'debug'
|
||||
theme: 'base'
|
||||
---
|
||||
gitGraph
|
||||
commit
|
||||
branch hotfix
|
||||
@@ -1140,7 +1244,11 @@ Let's put them to use, and see how our sample diagram looks in different themes:
|
||||
### Forest Theme
|
||||
|
||||
```mermaid-example
|
||||
%%{init: { 'logLevel': 'debug', 'theme': 'forest' } }%%
|
||||
---
|
||||
config:
|
||||
logLevel: 'debug'
|
||||
theme: 'forest'
|
||||
---
|
||||
gitGraph
|
||||
commit
|
||||
branch hotfix
|
||||
@@ -1186,7 +1294,11 @@ Let's put them to use, and see how our sample diagram looks in different themes:
|
||||
```
|
||||
|
||||
```mermaid
|
||||
%%{init: { 'logLevel': 'debug', 'theme': 'forest' } }%%
|
||||
---
|
||||
config:
|
||||
logLevel: 'debug'
|
||||
theme: 'forest'
|
||||
---
|
||||
gitGraph
|
||||
commit
|
||||
branch hotfix
|
||||
@@ -1234,7 +1346,11 @@ Let's put them to use, and see how our sample diagram looks in different themes:
|
||||
### Default Theme
|
||||
|
||||
```mermaid-example
|
||||
%%{init: { 'logLevel': 'debug', 'theme': 'default' } }%%
|
||||
---
|
||||
config:
|
||||
logLevel: 'debug'
|
||||
theme: 'default'
|
||||
---
|
||||
gitGraph
|
||||
commit type:HIGHLIGHT
|
||||
branch hotfix
|
||||
@@ -1280,7 +1396,11 @@ Let's put them to use, and see how our sample diagram looks in different themes:
|
||||
```
|
||||
|
||||
```mermaid
|
||||
%%{init: { 'logLevel': 'debug', 'theme': 'default' } }%%
|
||||
---
|
||||
config:
|
||||
logLevel: 'debug'
|
||||
theme: 'default'
|
||||
---
|
||||
gitGraph
|
||||
commit type:HIGHLIGHT
|
||||
branch hotfix
|
||||
@@ -1328,7 +1448,11 @@ Let's put them to use, and see how our sample diagram looks in different themes:
|
||||
### Dark Theme
|
||||
|
||||
```mermaid-example
|
||||
%%{init: { 'logLevel': 'debug', 'theme': 'dark' } }%%
|
||||
---
|
||||
config:
|
||||
logLevel: 'debug'
|
||||
theme: 'dark'
|
||||
---
|
||||
gitGraph
|
||||
commit
|
||||
branch hotfix
|
||||
@@ -1374,7 +1498,11 @@ Let's put them to use, and see how our sample diagram looks in different themes:
|
||||
```
|
||||
|
||||
```mermaid
|
||||
%%{init: { 'logLevel': 'debug', 'theme': 'dark' } }%%
|
||||
---
|
||||
config:
|
||||
logLevel: 'debug'
|
||||
theme: 'dark'
|
||||
---
|
||||
gitGraph
|
||||
commit
|
||||
branch hotfix
|
||||
@@ -1422,7 +1550,11 @@ Let's put them to use, and see how our sample diagram looks in different themes:
|
||||
### Neutral Theme
|
||||
|
||||
```mermaid-example
|
||||
%%{init: { 'logLevel': 'debug', 'theme': 'neutral' } }%%
|
||||
---
|
||||
config:
|
||||
logLevel: 'debug'
|
||||
theme: 'neutral'
|
||||
---
|
||||
gitGraph
|
||||
commit
|
||||
branch hotfix
|
||||
@@ -1468,7 +1600,11 @@ Let's put them to use, and see how our sample diagram looks in different themes:
|
||||
```
|
||||
|
||||
```mermaid
|
||||
%%{init: { 'logLevel': 'debug', 'theme': 'neutral' } }%%
|
||||
---
|
||||
config:
|
||||
logLevel: 'debug'
|
||||
theme: 'neutral'
|
||||
---
|
||||
gitGraph
|
||||
commit
|
||||
branch hotfix
|
||||
@@ -1522,7 +1658,11 @@ For understanding let us take a sample diagram with theme `default`, the default
|
||||
See how the default theme is used to set the colors for the branches:
|
||||
|
||||
```mermaid-example
|
||||
%%{init: { 'logLevel': 'debug', 'theme': 'default' } }%%
|
||||
---
|
||||
config:
|
||||
logLevel: 'debug'
|
||||
theme: 'default'
|
||||
---
|
||||
gitGraph
|
||||
commit
|
||||
branch develop
|
||||
@@ -1538,7 +1678,11 @@ See how the default theme is used to set the colors for the branches:
|
||||
```
|
||||
|
||||
```mermaid
|
||||
%%{init: { 'logLevel': 'debug', 'theme': 'default' } }%%
|
||||
---
|
||||
config:
|
||||
logLevel: 'debug'
|
||||
theme: 'default'
|
||||
---
|
||||
gitGraph
|
||||
commit
|
||||
branch develop
|
||||
@@ -1569,16 +1713,20 @@ Example:
|
||||
Now let's override the default values for the `git0` to `git3` variables:
|
||||
|
||||
```mermaid-example
|
||||
%%{init: { 'logLevel': 'debug', 'theme': 'default' , 'themeVariables': {
|
||||
'git0': '#ff0000',
|
||||
'git1': '#00ff00',
|
||||
'git2': '#0000ff',
|
||||
'git3': '#ff00ff',
|
||||
'git4': '#00ffff',
|
||||
'git5': '#ffff00',
|
||||
'git6': '#ff00ff',
|
||||
'git7': '#00ffff'
|
||||
} } }%%
|
||||
---
|
||||
config:
|
||||
logLevel: 'debug'
|
||||
theme: 'default'
|
||||
themeVariables:
|
||||
'git0': '#ff0000'
|
||||
'git1': '#00ff00'
|
||||
'git2': '#0000ff'
|
||||
'git3': '#ff00ff'
|
||||
'git4': '#00ffff'
|
||||
'git5': '#ffff00'
|
||||
'git6': '#ff00ff'
|
||||
'git7': '#00ffff'
|
||||
---
|
||||
gitGraph
|
||||
commit
|
||||
branch develop
|
||||
@@ -1595,16 +1743,20 @@ Now let's override the default values for the `git0` to `git3` variables:
|
||||
```
|
||||
|
||||
```mermaid
|
||||
%%{init: { 'logLevel': 'debug', 'theme': 'default' , 'themeVariables': {
|
||||
'git0': '#ff0000',
|
||||
'git1': '#00ff00',
|
||||
'git2': '#0000ff',
|
||||
'git3': '#ff00ff',
|
||||
'git4': '#00ffff',
|
||||
'git5': '#ffff00',
|
||||
'git6': '#ff00ff',
|
||||
'git7': '#00ffff'
|
||||
} } }%%
|
||||
---
|
||||
config:
|
||||
logLevel: 'debug'
|
||||
theme: 'default'
|
||||
themeVariables:
|
||||
'git0': '#ff0000'
|
||||
'git1': '#00ff00'
|
||||
'git2': '#0000ff'
|
||||
'git3': '#ff00ff'
|
||||
'git4': '#00ffff'
|
||||
'git5': '#ffff00'
|
||||
'git6': '#ff00ff'
|
||||
'git7': '#00ffff'
|
||||
---
|
||||
gitGraph
|
||||
commit
|
||||
branch develop
|
||||
@@ -1631,18 +1783,22 @@ Lets see how the default theme is used to set the colors for the branch labels:
|
||||
Now let's override the default values for the `gitBranchLabel0` to `gitBranchLabel2` variables:
|
||||
|
||||
```mermaid-example
|
||||
%%{init: { 'logLevel': 'debug', 'theme': 'default' , 'themeVariables': {
|
||||
'gitBranchLabel0': '#ffffff',
|
||||
'gitBranchLabel1': '#ffffff',
|
||||
'gitBranchLabel2': '#ffffff',
|
||||
'gitBranchLabel3': '#ffffff',
|
||||
'gitBranchLabel4': '#ffffff',
|
||||
'gitBranchLabel5': '#ffffff',
|
||||
'gitBranchLabel6': '#ffffff',
|
||||
'gitBranchLabel7': '#ffffff',
|
||||
'gitBranchLabel8': '#ffffff',
|
||||
'gitBranchLabel9': '#ffffff'
|
||||
} } }%%
|
||||
---
|
||||
config:
|
||||
logLevel: 'debug'
|
||||
theme: 'default'
|
||||
themeVariables:
|
||||
'gitBranchLabel0': '#ffffff'
|
||||
'gitBranchLabel1': '#ffffff'
|
||||
'gitBranchLabel2': '#ffffff'
|
||||
'gitBranchLabel3': '#ffffff'
|
||||
'gitBranchLabel4': '#ffffff'
|
||||
'gitBranchLabel5': '#ffffff'
|
||||
'gitBranchLabel6': '#ffffff'
|
||||
'gitBranchLabel7': '#ffffff'
|
||||
'gitBranchLabel8': '#ffffff'
|
||||
'gitBranchLabel9': '#ffffff'
|
||||
---
|
||||
gitGraph
|
||||
checkout main
|
||||
branch branch1
|
||||
@@ -1659,18 +1815,22 @@ Now let's override the default values for the `gitBranchLabel0` to `gitBranchLab
|
||||
```
|
||||
|
||||
```mermaid
|
||||
%%{init: { 'logLevel': 'debug', 'theme': 'default' , 'themeVariables': {
|
||||
'gitBranchLabel0': '#ffffff',
|
||||
'gitBranchLabel1': '#ffffff',
|
||||
'gitBranchLabel2': '#ffffff',
|
||||
'gitBranchLabel3': '#ffffff',
|
||||
'gitBranchLabel4': '#ffffff',
|
||||
'gitBranchLabel5': '#ffffff',
|
||||
'gitBranchLabel6': '#ffffff',
|
||||
'gitBranchLabel7': '#ffffff',
|
||||
'gitBranchLabel8': '#ffffff',
|
||||
'gitBranchLabel9': '#ffffff'
|
||||
} } }%%
|
||||
---
|
||||
config:
|
||||
logLevel: 'debug'
|
||||
theme: 'default'
|
||||
themeVariables:
|
||||
'gitBranchLabel0': '#ffffff'
|
||||
'gitBranchLabel1': '#ffffff'
|
||||
'gitBranchLabel2': '#ffffff'
|
||||
'gitBranchLabel3': '#ffffff'
|
||||
'gitBranchLabel4': '#ffffff'
|
||||
'gitBranchLabel5': '#ffffff'
|
||||
'gitBranchLabel6': '#ffffff'
|
||||
'gitBranchLabel7': '#ffffff'
|
||||
'gitBranchLabel8': '#ffffff'
|
||||
'gitBranchLabel9': '#ffffff'
|
||||
---
|
||||
gitGraph
|
||||
checkout main
|
||||
branch branch1
|
||||
@@ -1696,10 +1856,14 @@ Example:
|
||||
Now let's override the default values for the `commitLabelColor` to `commitLabelBackground` variables:
|
||||
|
||||
```mermaid-example
|
||||
%%{init: { 'logLevel': 'debug', 'theme': 'default' , 'themeVariables': {
|
||||
'commitLabelColor': '#ff0000',
|
||||
'commitLabelBackground': '#00ff00'
|
||||
} } }%%
|
||||
---
|
||||
config:
|
||||
logLevel: 'debug'
|
||||
theme: 'default'
|
||||
themeVariables:
|
||||
commitLabelColor: '#ff0000'
|
||||
commitLabelBackground: '#00ff00'
|
||||
---
|
||||
gitGraph
|
||||
commit
|
||||
branch develop
|
||||
@@ -1716,10 +1880,14 @@ Now let's override the default values for the `commitLabelColor` to `commitLabel
|
||||
```
|
||||
|
||||
```mermaid
|
||||
%%{init: { 'logLevel': 'debug', 'theme': 'default' , 'themeVariables': {
|
||||
'commitLabelColor': '#ff0000',
|
||||
'commitLabelBackground': '#00ff00'
|
||||
} } }%%
|
||||
---
|
||||
config:
|
||||
logLevel: 'debug'
|
||||
theme: 'default'
|
||||
themeVariables:
|
||||
commitLabelColor: '#ff0000'
|
||||
commitLabelBackground: '#00ff00'
|
||||
---
|
||||
gitGraph
|
||||
commit
|
||||
branch develop
|
||||
@@ -1745,11 +1913,15 @@ Example:
|
||||
Now let's override the default values for the `commitLabelFontSize` variable:
|
||||
|
||||
```mermaid-example
|
||||
%%{init: { 'logLevel': 'debug', 'theme': 'default' , 'themeVariables': {
|
||||
'commitLabelColor': '#ff0000',
|
||||
'commitLabelBackground': '#00ff00',
|
||||
'commitLabelFontSize': '16px'
|
||||
} } }%%
|
||||
---
|
||||
config:
|
||||
logLevel: 'debug'
|
||||
theme: 'default'
|
||||
themeVariables:
|
||||
commitLabelColor: '#ff0000'
|
||||
commitLabelBackground: '#00ff00'
|
||||
commitLabelFontSize: '16px'
|
||||
---
|
||||
gitGraph
|
||||
commit
|
||||
branch develop
|
||||
@@ -1766,11 +1938,15 @@ Now let's override the default values for the `commitLabelFontSize` variable:
|
||||
```
|
||||
|
||||
```mermaid
|
||||
%%{init: { 'logLevel': 'debug', 'theme': 'default' , 'themeVariables': {
|
||||
'commitLabelColor': '#ff0000',
|
||||
'commitLabelBackground': '#00ff00',
|
||||
'commitLabelFontSize': '16px'
|
||||
} } }%%
|
||||
---
|
||||
config:
|
||||
logLevel: 'debug'
|
||||
theme: 'default'
|
||||
themeVariables:
|
||||
commitLabelColor: '#ff0000'
|
||||
commitLabelBackground: '#00ff00'
|
||||
commitLabelFontSize: '16px'
|
||||
---
|
||||
gitGraph
|
||||
commit
|
||||
branch develop
|
||||
@@ -1796,11 +1972,15 @@ Example:
|
||||
Now let's override the default values for the `tagLabelFontSize` variable:
|
||||
|
||||
```mermaid-example
|
||||
%%{init: { 'logLevel': 'debug', 'theme': 'default' , 'themeVariables': {
|
||||
'commitLabelColor': '#ff0000',
|
||||
'commitLabelBackground': '#00ff00',
|
||||
'tagLabelFontSize': '16px'
|
||||
} } }%%
|
||||
---
|
||||
config:
|
||||
logLevel: 'debug'
|
||||
theme: 'default'
|
||||
themeVariables:
|
||||
commitLabelColor: '#ff0000'
|
||||
commitLabelBackground: '#00ff00'
|
||||
tagLabelFontSize: '16px'
|
||||
---
|
||||
gitGraph
|
||||
commit
|
||||
branch develop
|
||||
@@ -1817,11 +1997,15 @@ Now let's override the default values for the `tagLabelFontSize` variable:
|
||||
```
|
||||
|
||||
```mermaid
|
||||
%%{init: { 'logLevel': 'debug', 'theme': 'default' , 'themeVariables': {
|
||||
'commitLabelColor': '#ff0000',
|
||||
'commitLabelBackground': '#00ff00',
|
||||
'tagLabelFontSize': '16px'
|
||||
} } }%%
|
||||
---
|
||||
config:
|
||||
logLevel: 'debug'
|
||||
theme: 'default'
|
||||
themeVariables:
|
||||
commitLabelColor: '#ff0000'
|
||||
commitLabelBackground: '#00ff00'
|
||||
tagLabelFontSize: '16px'
|
||||
---
|
||||
gitGraph
|
||||
commit
|
||||
branch develop
|
||||
@@ -1846,11 +2030,15 @@ Example:
|
||||
Now let's override the default values for the `tagLabelColor`, `tagLabelBackground` and to `tagLabelBorder` variables:
|
||||
|
||||
```mermaid-example
|
||||
%%{init: { 'logLevel': 'debug', 'theme': 'default' , 'themeVariables': {
|
||||
'tagLabelColor': '#ff0000',
|
||||
'tagLabelBackground': '#00ff00',
|
||||
'tagLabelBorder': '#0000ff'
|
||||
} } }%%
|
||||
---
|
||||
config:
|
||||
logLevel: 'debug'
|
||||
theme: 'default'
|
||||
themeVariables:
|
||||
tagLabelColor: '#ff0000'
|
||||
tagLabelBackground: '#00ff00'
|
||||
tagLabelBorder: '#0000ff'
|
||||
---
|
||||
gitGraph
|
||||
commit
|
||||
branch develop
|
||||
@@ -1867,11 +2055,15 @@ Now let's override the default values for the `tagLabelColor`, `tagLabelBackgrou
|
||||
```
|
||||
|
||||
```mermaid
|
||||
%%{init: { 'logLevel': 'debug', 'theme': 'default' , 'themeVariables': {
|
||||
'tagLabelColor': '#ff0000',
|
||||
'tagLabelBackground': '#00ff00',
|
||||
'tagLabelBorder': '#0000ff'
|
||||
} } }%%
|
||||
---
|
||||
config:
|
||||
logLevel: 'debug'
|
||||
theme: 'default'
|
||||
themeVariables:
|
||||
tagLabelColor: '#ff0000'
|
||||
tagLabelBackground: '#00ff00'
|
||||
tagLabelBorder: '#0000ff'
|
||||
---
|
||||
gitGraph
|
||||
commit
|
||||
branch develop
|
||||
@@ -1898,9 +2090,13 @@ Example:
|
||||
Now let's override the default values for the `git0` to `git3` variables:
|
||||
|
||||
```mermaid-example
|
||||
%%{init: { 'logLevel': 'debug', 'theme': 'default' , 'themeVariables': {
|
||||
'gitInv0': '#ff0000'
|
||||
} } }%%
|
||||
---
|
||||
config:
|
||||
logLevel: 'debug'
|
||||
theme: 'default'
|
||||
themeVariables:
|
||||
'gitInv0': '#ff0000'
|
||||
---
|
||||
gitGraph
|
||||
commit
|
||||
branch develop
|
||||
@@ -1917,9 +2113,13 @@ Now let's override the default values for the `git0` to `git3` variables:
|
||||
```
|
||||
|
||||
```mermaid
|
||||
%%{init: { 'logLevel': 'debug', 'theme': 'default' , 'themeVariables': {
|
||||
'gitInv0': '#ff0000'
|
||||
} } }%%
|
||||
---
|
||||
config:
|
||||
logLevel: 'debug'
|
||||
theme: 'default'
|
||||
themeVariables:
|
||||
'gitInv0': '#ff0000'
|
||||
---
|
||||
gitGraph
|
||||
commit
|
||||
branch develop
|
||||
|
||||
@@ -107,17 +107,18 @@ xychart-beta
|
||||
|
||||
## Chart Configurations
|
||||
|
||||
| Parameter | Description | Default value |
|
||||
| ------------------------ | ---------------------------------------------- | :-----------: |
|
||||
| width | Width of the chart | 700 |
|
||||
| height | Height of the chart | 500 |
|
||||
| titlePadding | Top and Bottom padding of the title | 10 |
|
||||
| titleFontSize | Title font size | 20 |
|
||||
| showTitle | Title to be shown or not | true |
|
||||
| xAxis | xAxis configuration | AxisConfig |
|
||||
| yAxis | yAxis configuration | AxisConfig |
|
||||
| chartOrientation | 'vertical' or 'horizontal' | 'vertical' |
|
||||
| plotReservedSpacePercent | Minimum space plots will take inside the chart | 50 |
|
||||
| Parameter | Description | Default value |
|
||||
| ------------------------ | ------------------------------------------------------------- | :-----------: |
|
||||
| width | Width of the chart | 700 |
|
||||
| height | Height of the chart | 500 |
|
||||
| titlePadding | Top and Bottom padding of the title | 10 |
|
||||
| titleFontSize | Title font size | 20 |
|
||||
| showTitle | Title to be shown or not | true |
|
||||
| xAxis | xAxis configuration | AxisConfig |
|
||||
| yAxis | yAxis configuration | AxisConfig |
|
||||
| chartOrientation | 'vertical' or 'horizontal' | 'vertical' |
|
||||
| plotReservedSpacePercent | Minimum space plots will take inside the chart | 50 |
|
||||
| showDataLabel | Should show the value corresponding to the bar within the bar | false |
|
||||
|
||||
### AxisConfig
|
||||
|
||||
@@ -163,6 +164,7 @@ config:
|
||||
xyChart:
|
||||
width: 900
|
||||
height: 600
|
||||
showDataLabel: true
|
||||
themeVariables:
|
||||
xyChart:
|
||||
titleColor: "#ff0000"
|
||||
@@ -181,6 +183,7 @@ config:
|
||||
xyChart:
|
||||
width: 900
|
||||
height: 600
|
||||
showDataLabel: true
|
||||
themeVariables:
|
||||
xyChart:
|
||||
titleColor: "#ff0000"
|
||||
|
||||
22
package.json
22
package.json
@@ -64,12 +64,12 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@applitools/eyes-cypress": "^3.44.9",
|
||||
"@argos-ci/cypress": "^3.2.0",
|
||||
"@argos-ci/cypress": "^4.0.3",
|
||||
"@changesets/changelog-github": "^0.5.1",
|
||||
"@changesets/cli": "^2.27.12",
|
||||
"@cspell/eslint-plugin": "^8.8.4",
|
||||
"@cspell/eslint-plugin": "^8.18.1",
|
||||
"@cypress/code-coverage": "^3.12.49",
|
||||
"@eslint/js": "^9.4.0",
|
||||
"@eslint/js": "^9.24.0",
|
||||
"@rollup/plugin-typescript": "^12.1.2",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/express": "^5.0.0",
|
||||
@@ -83,7 +83,7 @@
|
||||
"@vitest/spy": "^3.0.6",
|
||||
"@vitest/ui": "^3.0.6",
|
||||
"ajv": "^8.17.1",
|
||||
"chokidar": "^3.6.0",
|
||||
"chokidar": "^4.0.3",
|
||||
"concurrently": "^9.1.2",
|
||||
"cors": "^2.8.5",
|
||||
"cpy-cli": "^5.0.0",
|
||||
@@ -93,19 +93,19 @@
|
||||
"cypress-image-snapshot": "^4.0.1",
|
||||
"cypress-split": "^1.24.14",
|
||||
"esbuild": "^0.25.0",
|
||||
"eslint": "^9.20.1",
|
||||
"eslint-config-prettier": "^10.0.0",
|
||||
"eslint-plugin-cypress": "^4.1.0",
|
||||
"eslint": "^9.24.0",
|
||||
"eslint-config-prettier": "^10.1.1",
|
||||
"eslint-plugin-cypress": "^4.2.1",
|
||||
"eslint-plugin-html": "^8.1.2",
|
||||
"eslint-plugin-jest": "^28.6.0",
|
||||
"eslint-plugin-jsdoc": "^50.0.1",
|
||||
"eslint-plugin-jest": "^28.11.0",
|
||||
"eslint-plugin-jsdoc": "^50.6.9",
|
||||
"eslint-plugin-json": "^4.0.1",
|
||||
"eslint-plugin-lodash": "^8.0.0",
|
||||
"eslint-plugin-markdown": "^5.1.0",
|
||||
"eslint-plugin-no-only-tests": "^3.3.0",
|
||||
"eslint-plugin-tsdoc": "^0.4.0",
|
||||
"eslint-plugin-unicorn": "^58.0.0",
|
||||
"express": "^4.19.2",
|
||||
"express": "^5.1.0",
|
||||
"globals": "^16.0.0",
|
||||
"globby": "^14.0.2",
|
||||
"husky": "^9.1.7",
|
||||
@@ -126,7 +126,7 @@
|
||||
"tslib": "^2.8.1",
|
||||
"tsx": "^4.7.3",
|
||||
"typescript": "~5.7.3",
|
||||
"typescript-eslint": "^8.24.1",
|
||||
"typescript-eslint": "^8.29.1",
|
||||
"vite": "^6.1.1",
|
||||
"vite-plugin-istanbul": "^7.0.0",
|
||||
"vitest": "^3.0.6"
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
"d3-sankey": "^0.12.3",
|
||||
"dagre-d3-es": "7.0.11",
|
||||
"dayjs": "^1.11.13",
|
||||
"dompurify": "^3.2.4",
|
||||
"dompurify": "^3.2.5",
|
||||
"katex": "^0.16.9",
|
||||
"khroma": "^2.1.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
|
||||
@@ -559,6 +559,10 @@ export interface JourneyDiagramConfig extends BaseDiagramConfig {
|
||||
* Margin between actors
|
||||
*/
|
||||
leftMargin?: number;
|
||||
/**
|
||||
* Maximum width of actor labels
|
||||
*/
|
||||
maxLabelWidth?: number;
|
||||
/**
|
||||
* Width of actor boxes
|
||||
*/
|
||||
@@ -617,6 +621,18 @@ export interface JourneyDiagramConfig extends BaseDiagramConfig {
|
||||
actorColours?: string[];
|
||||
sectionFills?: string[];
|
||||
sectionColours?: string[];
|
||||
/**
|
||||
* Color of the title text in Journey Diagrams
|
||||
*/
|
||||
titleColor?: string;
|
||||
/**
|
||||
* Font family to be used for the title text in Journey Diagrams
|
||||
*/
|
||||
titleFontFamily?: string;
|
||||
/**
|
||||
* Font size to be used for the title text in Journey Diagrams
|
||||
*/
|
||||
titleFontSize?: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `MermaidConfig`'s JSON-Schema
|
||||
@@ -935,6 +951,10 @@ export interface XYChartConfig extends BaseDiagramConfig {
|
||||
* Top and bottom space from the chart title
|
||||
*/
|
||||
titlePadding?: number;
|
||||
/**
|
||||
* Should show the value corresponding to the bar within the bar
|
||||
*/
|
||||
showDataLabel?: boolean;
|
||||
/**
|
||||
* Should show the chart title
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import { it, describe, expect } from 'vitest';
|
||||
import { db } from './architectureDb.js';
|
||||
import { parser } from './architectureParser.js';
|
||||
|
||||
const {
|
||||
clear,
|
||||
getDiagramTitle,
|
||||
getAccTitle,
|
||||
getAccDescription,
|
||||
getServices,
|
||||
getGroups,
|
||||
getEdges,
|
||||
getJunctions,
|
||||
} = db;
|
||||
|
||||
describe('architecture diagrams', () => {
|
||||
beforeEach(() => {
|
||||
clear();
|
||||
});
|
||||
|
||||
describe('architecture diagram definitions', () => {
|
||||
it('should handle the architecture keyword', async () => {
|
||||
const str = `architecture-beta`;
|
||||
await expect(parser.parse(str)).resolves.not.toThrow();
|
||||
});
|
||||
|
||||
it('should handle an simple radar definition', async () => {
|
||||
const str = `architecture-beta
|
||||
service db
|
||||
`;
|
||||
await expect(parser.parse(str)).resolves.not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('should handle TitleAndAccessibilities', () => {
|
||||
it('should handle title on the first line', async () => {
|
||||
const str = `architecture-beta title Simple Architecture Diagram`;
|
||||
await expect(parser.parse(str)).resolves.not.toThrow();
|
||||
expect(getDiagramTitle()).toBe('Simple Architecture Diagram');
|
||||
});
|
||||
|
||||
it('should handle title on another line', async () => {
|
||||
const str = `architecture-beta
|
||||
title Simple Architecture Diagram
|
||||
`;
|
||||
await expect(parser.parse(str)).resolves.not.toThrow();
|
||||
expect(getDiagramTitle()).toBe('Simple Architecture Diagram');
|
||||
});
|
||||
|
||||
it('should handle accessibility title and description', async () => {
|
||||
const str = `architecture-beta
|
||||
accTitle: Accessibility Title
|
||||
accDescr: Accessibility Description
|
||||
`;
|
||||
await expect(parser.parse(str)).resolves.not.toThrow();
|
||||
expect(getAccTitle()).toBe('Accessibility Title');
|
||||
expect(getAccDescription()).toBe('Accessibility Description');
|
||||
});
|
||||
|
||||
it('should handle multiline accessibility description', async () => {
|
||||
const str = `architecture-beta
|
||||
accDescr {
|
||||
Accessibility Description
|
||||
}
|
||||
`;
|
||||
await expect(parser.parse(str)).resolves.not.toThrow();
|
||||
expect(getAccDescription()).toBe('Accessibility Description');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ArchitectureDiagramConfig } from '../../config.type.js';
|
||||
import DEFAULT_CONFIG from '../../defaultConfig.js';
|
||||
import { getConfig } from '../../diagram-api/diagramAPI.js';
|
||||
import { getConfig as commonGetConfig } from '../../config.js';
|
||||
import type { D3Element } from '../../types.js';
|
||||
import { ImperativeState } from '../../utils/imperativeState.js';
|
||||
import {
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
isArchitectureService,
|
||||
shiftPositionByArchitectureDirectionPair,
|
||||
} from './architectureTypes.js';
|
||||
import { cleanAndMerge } from '../../utils.js';
|
||||
|
||||
const DEFAULT_ARCHITECTURE_CONFIG: Required<ArchitectureDiagramConfig> =
|
||||
DEFAULT_CONFIG.architecture;
|
||||
@@ -316,6 +317,14 @@ const setElementForId = (id: string, element: D3Element) => {
|
||||
};
|
||||
const getElementById = (id: string) => state.records.elements[id];
|
||||
|
||||
const getConfig = (): Required<ArchitectureDiagramConfig> => {
|
||||
const config = cleanAndMerge({
|
||||
...DEFAULT_ARCHITECTURE_CONFIG,
|
||||
...commonGetConfig().architecture,
|
||||
});
|
||||
return config;
|
||||
};
|
||||
|
||||
export const db: ArchitectureDB = {
|
||||
clear,
|
||||
setDiagramTitle,
|
||||
@@ -324,6 +333,7 @@ export const db: ArchitectureDB = {
|
||||
getAccTitle,
|
||||
setAccDescription,
|
||||
getAccDescription,
|
||||
getConfig,
|
||||
|
||||
addService,
|
||||
getServices,
|
||||
@@ -348,9 +358,5 @@ export const db: ArchitectureDB = {
|
||||
export function getConfigField<T extends keyof ArchitectureDiagramConfig>(
|
||||
field: T
|
||||
): Required<ArchitectureDiagramConfig>[T] {
|
||||
const arch = getConfig().architecture;
|
||||
if (arch?.[field]) {
|
||||
return arch[field] as Required<ArchitectureDiagramConfig>[T];
|
||||
}
|
||||
return DEFAULT_ARCHITECTURE_CONFIG[field];
|
||||
return getConfig()[field];
|
||||
}
|
||||
|
||||
@@ -500,6 +500,8 @@ function layoutArchitecture(
|
||||
}
|
||||
|
||||
export const draw: DrawDefinition = async (text, id, _version, diagObj: Diagram) => {
|
||||
// TODO: Add title support for architecture diagrams
|
||||
|
||||
const db = diagObj.db as ArchitectureDB;
|
||||
|
||||
const services = db.getServices();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { DiagramDB } from '../../diagram-api/types.js';
|
||||
import type { DiagramDBBase } from '../../diagram-api/types.js';
|
||||
import type { ArchitectureDiagramConfig } from '../../config.type.js';
|
||||
import type { D3Element } from '../../types.js';
|
||||
import type cytoscape from 'cytoscape';
|
||||
@@ -242,7 +242,7 @@ export interface ArchitectureEdge<DT = ArchitectureDirection> {
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export interface ArchitectureDB extends DiagramDB {
|
||||
export interface ArchitectureDB extends DiagramDBBase<ArchitectureDiagramConfig> {
|
||||
clear: () => void;
|
||||
addService: (service: Omit<ArchitectureService, 'edges'>) => void;
|
||||
getServices: () => ArchitectureService[];
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { it, describe, expect } from 'vitest';
|
||||
import { db } from './db.js';
|
||||
import { parser } from './parser.js';
|
||||
import { renderer, relativeRadius, closedRoundCurve } from './renderer.js';
|
||||
import { relativeRadius, closedRoundCurve } from './renderer.js';
|
||||
import { Diagram } from '../../Diagram.js';
|
||||
import mermaidAPI from '../../mermaidAPI.js';
|
||||
import { a } from 'vitest/dist/chunks/suite.qtkXWc6R.js';
|
||||
import { buildRadarStyleOptions } from './styles.js';
|
||||
|
||||
const {
|
||||
clear,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { MermaidConfig } from '../../config.type.js';
|
||||
import { getConfig } from '../../diagram-api/diagramAPI.js';
|
||||
import { log } from '../../logger.js';
|
||||
import common from '../common/common.js';
|
||||
@@ -33,9 +34,10 @@ import {
|
||||
STMT_RELATION,
|
||||
STMT_STATE,
|
||||
} from './stateCommon.js';
|
||||
import type { Edge, NodeData, StateStmt, Stmt, StyleClass } from './stateDb.js';
|
||||
|
||||
// List of nodes created from the parsed diagram statement items
|
||||
let nodeDb = new Map();
|
||||
const nodeDb = new Map<string, NodeData>();
|
||||
|
||||
let graphItemCount = 0; // used to construct ids, etc.
|
||||
|
||||
@@ -43,18 +45,27 @@ let graphItemCount = 0; // used to construct ids, etc.
|
||||
* Create a standard string for the dom ID of an item.
|
||||
* If a type is given, insert that before the counter, preceded by the type spacer
|
||||
*
|
||||
* @param itemId
|
||||
* @param counter
|
||||
* @param {string | null} type
|
||||
* @param typeSpacer
|
||||
* @returns {string}
|
||||
*/
|
||||
export function stateDomId(itemId = '', counter = 0, type = '', typeSpacer = DOMID_TYPE_SPACER) {
|
||||
export function stateDomId(
|
||||
itemId = '',
|
||||
counter = 0,
|
||||
type: string | null = '',
|
||||
typeSpacer = DOMID_TYPE_SPACER
|
||||
) {
|
||||
const typeStr = type !== null && type.length > 0 ? `${typeSpacer}${type}` : '';
|
||||
return `${DOMID_STATE}-${itemId}${typeStr}-${counter}`;
|
||||
}
|
||||
|
||||
const setupDoc = (parentParsedItem, doc, diagramStates, nodes, edges, altFlag, look, classes) => {
|
||||
const setupDoc = (
|
||||
parentParsedItem: StateStmt | undefined,
|
||||
doc: Stmt[],
|
||||
diagramStates: Map<string, StateStmt>,
|
||||
nodes: NodeData[],
|
||||
edges: Edge[],
|
||||
altFlag: boolean,
|
||||
look: MermaidConfig['look'],
|
||||
classes: Map<string, StyleClass>
|
||||
) => {
|
||||
// graphItemCount = 0;
|
||||
log.trace('items', doc);
|
||||
doc.forEach((item) => {
|
||||
@@ -95,7 +106,7 @@ const setupDoc = (parentParsedItem, doc, diagramStates, nodes, edges, altFlag, l
|
||||
arrowTypeEnd: 'arrow_barb',
|
||||
style: G_EDGE_STYLE,
|
||||
labelStyle: '',
|
||||
label: common.sanitizeText(item.description, getConfig()),
|
||||
label: common.sanitizeText(item.description ?? '', getConfig()),
|
||||
arrowheadStyle: G_EDGE_ARROWHEADSTYLE,
|
||||
labelpos: G_EDGE_LABELPOS,
|
||||
labelType: G_EDGE_LABELTYPE,
|
||||
@@ -115,11 +126,10 @@ const setupDoc = (parentParsedItem, doc, diagramStates, nodes, edges, altFlag, l
|
||||
* Get the direction from the statement items.
|
||||
* Look through all of the documents (docs) in the parsedItems
|
||||
* Because is a _document_ direction, the default direction is not necessarily the same as the overall default _diagram_ direction.
|
||||
* @param {object[]} parsedItem - the parsed statement item to look through
|
||||
* @param [defaultDir] - the direction to use if none is found
|
||||
* @returns {string}
|
||||
* @param parsedItem - the parsed statement item to look through
|
||||
* @param defaultDir - the direction to use if none is found
|
||||
*/
|
||||
const getDir = (parsedItem, defaultDir = DEFAULT_NESTED_DOC_DIR) => {
|
||||
const getDir = (parsedItem: { doc?: Stmt[] }, defaultDir = DEFAULT_NESTED_DOC_DIR) => {
|
||||
let dir = defaultDir;
|
||||
if (parsedItem.doc) {
|
||||
for (const parsedItemDoc of parsedItem.doc) {
|
||||
@@ -131,7 +141,11 @@ const getDir = (parsedItem, defaultDir = DEFAULT_NESTED_DOC_DIR) => {
|
||||
return dir;
|
||||
};
|
||||
|
||||
function insertOrUpdateNode(nodes, nodeData, classes) {
|
||||
function insertOrUpdateNode(
|
||||
nodes: NodeData[],
|
||||
nodeData: NodeData,
|
||||
classes: Map<string, StyleClass>
|
||||
) {
|
||||
if (!nodeData.id || nodeData.id === '</join></fork>' || nodeData.id === '</choice>') {
|
||||
return;
|
||||
}
|
||||
@@ -143,9 +157,9 @@ function insertOrUpdateNode(nodes, nodeData, classes) {
|
||||
}
|
||||
|
||||
nodeData.cssClasses.split(' ').forEach((cssClass) => {
|
||||
if (classes.get(cssClass)) {
|
||||
const classDef = classes.get(cssClass);
|
||||
nodeData.cssCompiledStyles = [...nodeData.cssCompiledStyles, ...classDef.styles];
|
||||
const classDef = classes.get(cssClass);
|
||||
if (classDef) {
|
||||
nodeData.cssCompiledStyles = [...(nodeData.cssCompiledStyles ?? []), ...classDef.styles];
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -162,31 +176,30 @@ function insertOrUpdateNode(nodes, nodeData, classes) {
|
||||
* If there aren't any or if dbInfoItem isn't defined, return an empty string.
|
||||
* Else create 1 string from the list of classes found
|
||||
*
|
||||
* @param {undefined | null | object} dbInfoItem
|
||||
* @returns {string}
|
||||
*/
|
||||
function getClassesFromDbInfo(dbInfoItem) {
|
||||
function getClassesFromDbInfo(dbInfoItem?: StateStmt): string {
|
||||
return dbInfoItem?.classes?.join(' ') ?? '';
|
||||
}
|
||||
|
||||
function getStylesFromDbInfo(dbInfoItem) {
|
||||
function getStylesFromDbInfo(dbInfoItem?: StateStmt): string[] {
|
||||
return dbInfoItem?.styles ?? [];
|
||||
}
|
||||
|
||||
export const dataFetcher = (
|
||||
parent,
|
||||
parsedItem,
|
||||
diagramStates,
|
||||
nodes,
|
||||
edges,
|
||||
altFlag,
|
||||
look,
|
||||
classes
|
||||
parent: StateStmt | undefined,
|
||||
parsedItem: StateStmt,
|
||||
diagramStates: Map<string, StateStmt>,
|
||||
nodes: NodeData[],
|
||||
edges: Edge[],
|
||||
altFlag: boolean,
|
||||
look: MermaidConfig['look'],
|
||||
classes: Map<string, StyleClass>
|
||||
) => {
|
||||
const itemId = parsedItem.id;
|
||||
const dbState = diagramStates.get(itemId);
|
||||
const classStr = getClassesFromDbInfo(dbState);
|
||||
const style = getStylesFromDbInfo(dbState);
|
||||
const config = getConfig();
|
||||
|
||||
log.info('dataFetcher parsedItem', parsedItem, dbState, style);
|
||||
|
||||
@@ -207,13 +220,13 @@ export const dataFetcher = (
|
||||
nodeDb.set(itemId, {
|
||||
id: itemId,
|
||||
shape,
|
||||
description: common.sanitizeText(itemId, getConfig()),
|
||||
description: common.sanitizeText(itemId, config),
|
||||
cssClasses: `${classStr} ${CSS_DIAGRAM_STATE}`,
|
||||
cssStyles: style,
|
||||
});
|
||||
}
|
||||
|
||||
const newNode = nodeDb.get(itemId);
|
||||
const newNode = nodeDb.get(itemId)!;
|
||||
|
||||
// Save data for description and group so that for instance a statement without description overwrites
|
||||
// one with description @todo TODO What does this mean? If important, add a test for it
|
||||
@@ -225,7 +238,7 @@ export const dataFetcher = (
|
||||
newNode.shape = SHAPE_STATE_WITH_DESC;
|
||||
newNode.description.push(parsedItem.description);
|
||||
} else {
|
||||
if (newNode.description?.length > 0) {
|
||||
if (newNode.description?.length && newNode.description.length > 0) {
|
||||
// if there is a description already transform it to an array
|
||||
newNode.shape = SHAPE_STATE_WITH_DESC;
|
||||
if (newNode.description === itemId) {
|
||||
@@ -239,7 +252,7 @@ export const dataFetcher = (
|
||||
newNode.description = parsedItem.description;
|
||||
}
|
||||
}
|
||||
newNode.description = common.sanitizeTextOrArray(newNode.description, getConfig());
|
||||
newNode.description = common.sanitizeTextOrArray(newNode.description, config);
|
||||
}
|
||||
|
||||
// If there's only 1 description entry, just use a regular state shape
|
||||
@@ -262,7 +275,7 @@ export const dataFetcher = (
|
||||
}
|
||||
|
||||
// This is what will be added to the graph
|
||||
const nodeData = {
|
||||
const nodeData: NodeData = {
|
||||
labelStyle: '',
|
||||
shape: newNode.shape,
|
||||
label: newNode.description,
|
||||
@@ -294,19 +307,19 @@ export const dataFetcher = (
|
||||
|
||||
if (parsedItem.note) {
|
||||
// Todo: set random id
|
||||
const noteData = {
|
||||
const noteData: NodeData = {
|
||||
labelStyle: '',
|
||||
shape: SHAPE_NOTE,
|
||||
label: parsedItem.note.text,
|
||||
cssClasses: CSS_DIAGRAM_NOTE,
|
||||
// useHtmlLabels: false,
|
||||
cssStyles: [],
|
||||
cssCompilesStyles: [],
|
||||
cssCompiledStyles: [],
|
||||
id: itemId + NOTE_ID + '-' + graphItemCount,
|
||||
domId: stateDomId(itemId, graphItemCount, NOTE),
|
||||
type: newNode.type,
|
||||
isGroup: newNode.type === 'group',
|
||||
padding: getConfig().flowchart.padding,
|
||||
padding: config.flowchart?.padding,
|
||||
look,
|
||||
position: parsedItem.note.position,
|
||||
};
|
||||
@@ -1,16 +0,0 @@
|
||||
const idCache = {};
|
||||
|
||||
export const set = (key, val) => {
|
||||
idCache[key] = val;
|
||||
};
|
||||
|
||||
export const get = (k) => idCache[k];
|
||||
export const keys = () => Object.keys(idCache);
|
||||
export const size = () => keys().length;
|
||||
|
||||
export default {
|
||||
get,
|
||||
set,
|
||||
keys,
|
||||
size,
|
||||
};
|
||||
@@ -1,5 +1,4 @@
|
||||
import { line, curveBasis } from 'd3';
|
||||
import idCache from './id-cache.js';
|
||||
import { StateDB } from './stateDb.js';
|
||||
import utils from '../../utils.js';
|
||||
import common from '../common/common.js';
|
||||
@@ -405,8 +404,6 @@ export const drawState = function (elem, stateDef) {
|
||||
stateInfo.width = stateBox.width + 2 * getConfig().state.padding;
|
||||
stateInfo.height = stateBox.height + 2 * getConfig().state.padding;
|
||||
|
||||
idCache.set(id, stateInfo);
|
||||
// stateCnt++;
|
||||
return stateInfo;
|
||||
};
|
||||
|
||||
|
||||
@@ -13,6 +13,10 @@ export const STMT_DIRECTION = 'dir';
|
||||
|
||||
// parsed statement type for a state
|
||||
export const STMT_STATE = 'state';
|
||||
|
||||
// parsed statement type for a root
|
||||
export const STMT_ROOT = 'root';
|
||||
|
||||
// parsed statement type for a relation
|
||||
export const STMT_RELATION = 'relation';
|
||||
// parsed statement type for a classDef
|
||||
|
||||
@@ -1,706 +0,0 @@
|
||||
import { getConfig } from '../../diagram-api/diagramAPI.js';
|
||||
import { log } from '../../logger.js';
|
||||
import { generateId } from '../../utils.js';
|
||||
import common from '../common/common.js';
|
||||
import {
|
||||
clear as commonClear,
|
||||
getAccDescription,
|
||||
getAccTitle,
|
||||
getDiagramTitle,
|
||||
setAccDescription,
|
||||
setAccTitle,
|
||||
setDiagramTitle,
|
||||
} from '../common/commonDb.js';
|
||||
import { dataFetcher, reset as resetDataFetching } from './dataFetcher.js';
|
||||
import { getDir } from './stateRenderer-v3-unified.js';
|
||||
|
||||
import {
|
||||
DEFAULT_DIAGRAM_DIRECTION,
|
||||
DEFAULT_STATE_TYPE,
|
||||
DIVIDER_TYPE,
|
||||
STMT_APPLYCLASS,
|
||||
STMT_CLASSDEF,
|
||||
STMT_DIRECTION,
|
||||
STMT_RELATION,
|
||||
STMT_STATE,
|
||||
STMT_STYLEDEF,
|
||||
} from './stateCommon.js';
|
||||
|
||||
const START_NODE = '[*]';
|
||||
const START_TYPE = 'start';
|
||||
const END_NODE = START_NODE;
|
||||
const END_TYPE = 'end';
|
||||
|
||||
const COLOR_KEYWORD = 'color';
|
||||
const FILL_KEYWORD = 'fill';
|
||||
const BG_FILL = 'bgFill';
|
||||
const STYLECLASS_SEP = ',';
|
||||
|
||||
/**
|
||||
* Returns a new list of classes.
|
||||
* In the future, this can be replaced with a class common to all diagrams.
|
||||
* ClassDef information = { id: id, styles: [], textStyles: [] }
|
||||
*
|
||||
* @returns {Map<string, any>}
|
||||
*/
|
||||
function newClassesList() {
|
||||
return new Map();
|
||||
}
|
||||
|
||||
const newDoc = () => {
|
||||
return {
|
||||
/** @type {{ id1: string, id2: string, relationTitle: string }[]} */
|
||||
relations: [],
|
||||
states: new Map(),
|
||||
documents: {},
|
||||
};
|
||||
};
|
||||
|
||||
const clone = (o) => JSON.parse(JSON.stringify(o));
|
||||
|
||||
export class StateDB {
|
||||
/**
|
||||
* @param {1 | 2} version - v1 renderer or v2 renderer.
|
||||
*/
|
||||
constructor(version) {
|
||||
this.clear();
|
||||
|
||||
this.version = version;
|
||||
|
||||
// Needed for JISON since it only supports direct properties
|
||||
this.setRootDoc = this.setRootDoc.bind(this);
|
||||
this.getDividerId = this.getDividerId.bind(this);
|
||||
this.setDirection = this.setDirection.bind(this);
|
||||
this.trimColon = this.trimColon.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @type {1 | 2}
|
||||
*/
|
||||
version;
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @type {Array}
|
||||
*/
|
||||
nodes = [];
|
||||
/**
|
||||
* @private
|
||||
* @type {Array}
|
||||
*/
|
||||
edges = [];
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @type {Array}
|
||||
*/
|
||||
rootDoc = [];
|
||||
/**
|
||||
* @private
|
||||
* @type {Map<string, any>}
|
||||
*/
|
||||
classes = newClassesList(); // style classes defined by a classDef
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @type {Object}
|
||||
*/
|
||||
documents = {
|
||||
root: newDoc(),
|
||||
};
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @type {Object}
|
||||
*/
|
||||
currentDocument = this.documents.root;
|
||||
/**
|
||||
* @private
|
||||
* @type {number}
|
||||
*/
|
||||
startEndCount = 0;
|
||||
/**
|
||||
* @private
|
||||
* @type {number}
|
||||
*/
|
||||
dividerCnt = 0;
|
||||
|
||||
static relationType = {
|
||||
AGGREGATION: 0,
|
||||
EXTENSION: 1,
|
||||
COMPOSITION: 2,
|
||||
DEPENDENCY: 3,
|
||||
};
|
||||
|
||||
setRootDoc(o) {
|
||||
log.info('Setting root doc', o);
|
||||
// rootDoc = { id: 'root', doc: o };
|
||||
this.rootDoc = o;
|
||||
if (this.version === 1) {
|
||||
this.extract(o);
|
||||
} else {
|
||||
this.extract(this.getRootDocV2());
|
||||
}
|
||||
}
|
||||
|
||||
getRootDoc() {
|
||||
return this.rootDoc;
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {Object} parent
|
||||
* @param {Object} node
|
||||
* @param {boolean} first
|
||||
*/
|
||||
docTranslator(parent, node, first) {
|
||||
if (node.stmt === STMT_RELATION) {
|
||||
this.docTranslator(parent, node.state1, true);
|
||||
this.docTranslator(parent, node.state2, false);
|
||||
} else {
|
||||
if (node.stmt === STMT_STATE) {
|
||||
if (node.id === '[*]') {
|
||||
node.id = first ? parent.id + '_start' : parent.id + '_end';
|
||||
node.start = first;
|
||||
} else {
|
||||
// This is just a plain state, not a start or end
|
||||
node.id = node.id.trim();
|
||||
}
|
||||
}
|
||||
|
||||
if (node.doc) {
|
||||
const doc = [];
|
||||
// Check for concurrency
|
||||
let currentDoc = [];
|
||||
let i;
|
||||
for (i = 0; i < node.doc.length; i++) {
|
||||
if (node.doc[i].type === DIVIDER_TYPE) {
|
||||
const newNode = clone(node.doc[i]);
|
||||
newNode.doc = clone(currentDoc);
|
||||
doc.push(newNode);
|
||||
currentDoc = [];
|
||||
} else {
|
||||
currentDoc.push(node.doc[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// If any divider was encountered
|
||||
if (doc.length > 0 && currentDoc.length > 0) {
|
||||
const newNode = {
|
||||
stmt: STMT_STATE,
|
||||
id: generateId(),
|
||||
type: 'divider',
|
||||
doc: clone(currentDoc),
|
||||
};
|
||||
doc.push(clone(newNode));
|
||||
node.doc = doc;
|
||||
}
|
||||
|
||||
node.doc.forEach((docNode) => this.docTranslator(node, docNode, true));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
getRootDocV2() {
|
||||
this.docTranslator({ id: 'root' }, { id: 'root', doc: this.rootDoc }, true);
|
||||
return { id: 'root', doc: this.rootDoc };
|
||||
// Here
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert all of the statements (stmts) that were parsed into states and relationships.
|
||||
* This is done because a state diagram may have nested sections,
|
||||
* where each section is a 'document' and has its own set of statements.
|
||||
* Ex: the section within a fork has its own statements, and incoming and outgoing statements
|
||||
* refer to the fork as a whole (document).
|
||||
* See the parser grammar: the definition of a document is a document then a 'line', where a line can be a statement.
|
||||
* This will push the statement into the list of statements for the current document.
|
||||
* @private
|
||||
* @param _doc
|
||||
*/
|
||||
extract(_doc) {
|
||||
// const res = { states: [], relations: [] };
|
||||
let doc;
|
||||
if (_doc.doc) {
|
||||
doc = _doc.doc;
|
||||
} else {
|
||||
doc = _doc;
|
||||
}
|
||||
// let doc = root.doc;
|
||||
// if (!doc) {
|
||||
// doc = root;
|
||||
// }
|
||||
log.info(doc);
|
||||
this.clear(true);
|
||||
|
||||
log.info('Extract initial document:', doc);
|
||||
|
||||
doc.forEach((item) => {
|
||||
log.warn('Statement', item.stmt);
|
||||
switch (item.stmt) {
|
||||
case STMT_STATE:
|
||||
this.addState(
|
||||
item.id.trim(),
|
||||
item.type,
|
||||
item.doc,
|
||||
item.description,
|
||||
item.note,
|
||||
item.classes,
|
||||
item.styles,
|
||||
item.textStyles
|
||||
);
|
||||
break;
|
||||
case STMT_RELATION:
|
||||
this.addRelation(item.state1, item.state2, item.description);
|
||||
break;
|
||||
case STMT_CLASSDEF:
|
||||
this.addStyleClass(item.id.trim(), item.classes);
|
||||
break;
|
||||
case STMT_STYLEDEF:
|
||||
{
|
||||
const ids = item.id.trim().split(',');
|
||||
const styles = item.styleClass.split(',');
|
||||
ids.forEach((id) => {
|
||||
let foundState = this.getState(id);
|
||||
if (foundState === undefined) {
|
||||
const trimmedId = id.trim();
|
||||
this.addState(trimmedId);
|
||||
foundState = this.getState(trimmedId);
|
||||
}
|
||||
foundState.styles = styles.map((s) => s.replace(/;/g, '')?.trim());
|
||||
});
|
||||
}
|
||||
break;
|
||||
case STMT_APPLYCLASS:
|
||||
this.setCssClass(item.id.trim(), item.styleClass);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
const diagramStates = this.getStates();
|
||||
const config = getConfig();
|
||||
const look = config.look;
|
||||
|
||||
resetDataFetching();
|
||||
dataFetcher(
|
||||
undefined,
|
||||
this.getRootDocV2(),
|
||||
diagramStates,
|
||||
this.nodes,
|
||||
this.edges,
|
||||
true,
|
||||
look,
|
||||
this.classes
|
||||
);
|
||||
this.nodes.forEach((node) => {
|
||||
if (Array.isArray(node.label)) {
|
||||
// add the rest as description
|
||||
node.description = node.label.slice(1);
|
||||
if (node.isGroup && node.description.length > 0) {
|
||||
throw new Error(
|
||||
'Group nodes can only have label. Remove the additional description for node [' +
|
||||
node.id +
|
||||
']'
|
||||
);
|
||||
}
|
||||
// add first description as label
|
||||
node.label = node.label[0];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Function called by parser when a node definition has been found.
|
||||
*
|
||||
* @param {null | string} id
|
||||
* @param {null | string} type
|
||||
* @param {null | string} doc
|
||||
* @param {null | string | string[]} descr - description for the state. Can be a string or a list or strings
|
||||
* @param {null | string} note
|
||||
* @param {null | string | string[]} classes - class styles to apply to this state. Can be a string (1 style) or an array of styles. If it's just 1 class, convert it to an array of that 1 class.
|
||||
* @param {null | string | string[]} styles - styles to apply to this state. Can be a string (1 style) or an array of styles. If it's just 1 style, convert it to an array of that 1 style.
|
||||
* @param {null | string | string[]} textStyles - text styles to apply to this state. Can be a string (1 text test) or an array of text styles. If it's just 1 text style, convert it to an array of that 1 text style.
|
||||
*/
|
||||
addState(
|
||||
id,
|
||||
type = DEFAULT_STATE_TYPE,
|
||||
doc = null,
|
||||
descr = null,
|
||||
note = null,
|
||||
classes = null,
|
||||
styles = null,
|
||||
textStyles = null
|
||||
) {
|
||||
const trimmedId = id?.trim();
|
||||
// add the state if needed
|
||||
if (!this.currentDocument.states.has(trimmedId)) {
|
||||
log.info('Adding state ', trimmedId, descr);
|
||||
this.currentDocument.states.set(trimmedId, {
|
||||
id: trimmedId,
|
||||
descriptions: [],
|
||||
type,
|
||||
doc,
|
||||
note,
|
||||
classes: [],
|
||||
styles: [],
|
||||
textStyles: [],
|
||||
});
|
||||
} else {
|
||||
if (!this.currentDocument.states.get(trimmedId).doc) {
|
||||
this.currentDocument.states.get(trimmedId).doc = doc;
|
||||
}
|
||||
if (!this.currentDocument.states.get(trimmedId).type) {
|
||||
this.currentDocument.states.get(trimmedId).type = type;
|
||||
}
|
||||
}
|
||||
|
||||
if (descr) {
|
||||
log.info('Setting state description', trimmedId, descr);
|
||||
if (typeof descr === 'string') {
|
||||
this.addDescription(trimmedId, descr.trim());
|
||||
}
|
||||
|
||||
if (typeof descr === 'object') {
|
||||
descr.forEach((des) => this.addDescription(trimmedId, des.trim()));
|
||||
}
|
||||
}
|
||||
|
||||
if (note) {
|
||||
const doc2 = this.currentDocument.states.get(trimmedId);
|
||||
doc2.note = note;
|
||||
doc2.note.text = common.sanitizeText(doc2.note.text, getConfig());
|
||||
}
|
||||
|
||||
if (classes) {
|
||||
log.info('Setting state classes', trimmedId, classes);
|
||||
const classesList = typeof classes === 'string' ? [classes] : classes;
|
||||
classesList.forEach((cssClass) => this.setCssClass(trimmedId, cssClass.trim()));
|
||||
}
|
||||
|
||||
if (styles) {
|
||||
log.info('Setting state styles', trimmedId, styles);
|
||||
const stylesList = typeof styles === 'string' ? [styles] : styles;
|
||||
stylesList.forEach((style) => this.setStyle(trimmedId, style.trim()));
|
||||
}
|
||||
|
||||
if (textStyles) {
|
||||
log.info('Setting state styles', trimmedId, styles);
|
||||
const textStylesList = typeof textStyles === 'string' ? [textStyles] : textStyles;
|
||||
textStylesList.forEach((textStyle) => this.setTextStyle(trimmedId, textStyle.trim()));
|
||||
}
|
||||
}
|
||||
|
||||
clear(saveCommon) {
|
||||
this.nodes = [];
|
||||
this.edges = [];
|
||||
this.documents = {
|
||||
root: newDoc(),
|
||||
};
|
||||
this.currentDocument = this.documents.root;
|
||||
|
||||
// number of start and end nodes; used to construct ids
|
||||
this.startEndCount = 0;
|
||||
this.classes = newClassesList();
|
||||
if (!saveCommon) {
|
||||
commonClear();
|
||||
}
|
||||
}
|
||||
|
||||
getState(id) {
|
||||
return this.currentDocument.states.get(id);
|
||||
}
|
||||
getStates() {
|
||||
return this.currentDocument.states;
|
||||
}
|
||||
logDocuments() {
|
||||
log.info('Documents = ', this.documents);
|
||||
}
|
||||
getRelations() {
|
||||
return this.currentDocument.relations;
|
||||
}
|
||||
|
||||
/**
|
||||
* If the id is a start node ( [*] ), then return a new id constructed from
|
||||
* the start node name and the current start node count.
|
||||
* else return the given id
|
||||
*
|
||||
* @param {string} id
|
||||
* @returns {string} - the id (original or constructed)
|
||||
* @private
|
||||
*/
|
||||
startIdIfNeeded(id = '') {
|
||||
let fixedId = id;
|
||||
if (id === START_NODE) {
|
||||
this.startEndCount++;
|
||||
fixedId = `${START_TYPE}${this.startEndCount}`;
|
||||
}
|
||||
return fixedId;
|
||||
}
|
||||
|
||||
/**
|
||||
* If the id is a start node ( [*] ), then return the start type ('start')
|
||||
* else return the given type
|
||||
*
|
||||
* @param {string} id
|
||||
* @param {string} type
|
||||
* @returns {string} - the type that should be used
|
||||
* @private
|
||||
*/
|
||||
startTypeIfNeeded(id = '', type = DEFAULT_STATE_TYPE) {
|
||||
return id === START_NODE ? START_TYPE : type;
|
||||
}
|
||||
|
||||
/**
|
||||
* If the id is an end node ( [*] ), then return a new id constructed from
|
||||
* the end node name and the current start_end node count.
|
||||
* else return the given id
|
||||
*
|
||||
* @param {string} id
|
||||
* @returns {string} - the id (original or constructed)
|
||||
* @private
|
||||
*/
|
||||
endIdIfNeeded(id = '') {
|
||||
let fixedId = id;
|
||||
if (id === END_NODE) {
|
||||
this.startEndCount++;
|
||||
fixedId = `${END_TYPE}${this.startEndCount}`;
|
||||
}
|
||||
return fixedId;
|
||||
}
|
||||
|
||||
/**
|
||||
* If the id is an end node ( [*] ), then return the end type
|
||||
* else return the given type
|
||||
*
|
||||
* @param {string} id
|
||||
* @param {string} type
|
||||
* @returns {string} - the type that should be used
|
||||
* @private
|
||||
*/
|
||||
endTypeIfNeeded(id = '', type = DEFAULT_STATE_TYPE) {
|
||||
return id === END_NODE ? END_TYPE : type;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param item1
|
||||
* @param item2
|
||||
* @param relationTitle
|
||||
*/
|
||||
addRelationObjs(item1, item2, relationTitle) {
|
||||
let id1 = this.startIdIfNeeded(item1.id.trim());
|
||||
let type1 = this.startTypeIfNeeded(item1.id.trim(), item1.type);
|
||||
let id2 = this.startIdIfNeeded(item2.id.trim());
|
||||
let type2 = this.startTypeIfNeeded(item2.id.trim(), item2.type);
|
||||
|
||||
this.addState(
|
||||
id1,
|
||||
type1,
|
||||
item1.doc,
|
||||
item1.description,
|
||||
item1.note,
|
||||
item1.classes,
|
||||
item1.styles,
|
||||
item1.textStyles
|
||||
);
|
||||
this.addState(
|
||||
id2,
|
||||
type2,
|
||||
item2.doc,
|
||||
item2.description,
|
||||
item2.note,
|
||||
item2.classes,
|
||||
item2.styles,
|
||||
item2.textStyles
|
||||
);
|
||||
|
||||
this.currentDocument.relations.push({
|
||||
id1,
|
||||
id2,
|
||||
relationTitle: common.sanitizeText(relationTitle, getConfig()),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a relation between two items. The items may be full objects or just the string id of a state.
|
||||
*
|
||||
* @param {string | object} item1
|
||||
* @param {string | object} item2
|
||||
* @param {string} title
|
||||
*/
|
||||
addRelation(item1, item2, title) {
|
||||
if (typeof item1 === 'object') {
|
||||
this.addRelationObjs(item1, item2, title);
|
||||
} else {
|
||||
const id1 = this.startIdIfNeeded(item1.trim());
|
||||
const type1 = this.startTypeIfNeeded(item1);
|
||||
const id2 = this.endIdIfNeeded(item2.trim());
|
||||
const type2 = this.endTypeIfNeeded(item2);
|
||||
|
||||
this.addState(id1, type1);
|
||||
this.addState(id2, type2);
|
||||
this.currentDocument.relations.push({
|
||||
id1,
|
||||
id2,
|
||||
title: common.sanitizeText(title, getConfig()),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
addDescription(id, descr) {
|
||||
const theState = this.currentDocument.states.get(id);
|
||||
const _descr = descr.startsWith(':') ? descr.replace(':', '').trim() : descr;
|
||||
theState.descriptions.push(common.sanitizeText(_descr, getConfig()));
|
||||
}
|
||||
|
||||
cleanupLabel(label) {
|
||||
if (label.substring(0, 1) === ':') {
|
||||
return label.substr(2).trim();
|
||||
} else {
|
||||
return label.trim();
|
||||
}
|
||||
}
|
||||
|
||||
getDividerId() {
|
||||
this.dividerCnt++;
|
||||
return 'divider-id-' + this.dividerCnt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the parser comes across a (style) class definition
|
||||
* @example classDef my-style fill:#f96;
|
||||
*
|
||||
* @param {string} id - the id of this (style) class
|
||||
* @param {string | null} styleAttributes - the string with 1 or more style attributes (each separated by a comma)
|
||||
*/
|
||||
addStyleClass(id, styleAttributes = '') {
|
||||
// create a new style class object with this id
|
||||
if (!this.classes.has(id)) {
|
||||
this.classes.set(id, { id: id, styles: [], textStyles: [] }); // This is a classDef
|
||||
}
|
||||
const foundClass = this.classes.get(id);
|
||||
if (styleAttributes !== undefined && styleAttributes !== null) {
|
||||
styleAttributes.split(STYLECLASS_SEP).forEach((attrib) => {
|
||||
// remove any trailing ;
|
||||
const fixedAttrib = attrib.replace(/([^;]*);/, '$1').trim();
|
||||
|
||||
// replace some style keywords
|
||||
if (RegExp(COLOR_KEYWORD).exec(attrib)) {
|
||||
const newStyle1 = fixedAttrib.replace(FILL_KEYWORD, BG_FILL);
|
||||
const newStyle2 = newStyle1.replace(COLOR_KEYWORD, FILL_KEYWORD);
|
||||
foundClass.textStyles.push(newStyle2);
|
||||
}
|
||||
foundClass.styles.push(fixedAttrib);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all of the style classes
|
||||
* @returns {{} | any | classes}
|
||||
*/
|
||||
getClasses() {
|
||||
return this.classes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a (style) class or css class to a state with the given id.
|
||||
* If the state isn't already in the list of known states, add it.
|
||||
* Might be called by parser when a style class or CSS class should be applied to a state
|
||||
*
|
||||
* @param {string | string[]} itemIds The id or a list of ids of the item(s) to apply the css class to
|
||||
* @param {string} cssClassName CSS class name
|
||||
*/
|
||||
setCssClass(itemIds, cssClassName) {
|
||||
itemIds.split(',').forEach((id) => {
|
||||
let foundState = this.getState(id);
|
||||
if (foundState === undefined) {
|
||||
const trimmedId = id.trim();
|
||||
this.addState(trimmedId);
|
||||
foundState = this.getState(trimmedId);
|
||||
}
|
||||
foundState.classes.push(cssClassName);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a style to a state with the given id.
|
||||
* @example style stateId fill:#f9f,stroke:#333,stroke-width:4px
|
||||
* where 'style' is the keyword
|
||||
* stateId is the id of a state
|
||||
* the rest of the string is the styleText (all of the attributes to be applied to the state)
|
||||
*
|
||||
* @param itemId The id of item to apply the style to
|
||||
* @param styleText - the text of the attributes for the style
|
||||
*/
|
||||
setStyle(itemId, styleText) {
|
||||
const item = this.getState(itemId);
|
||||
if (item !== undefined) {
|
||||
item.styles.push(styleText);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a text style to a state with the given id
|
||||
*
|
||||
* @param itemId The id of item to apply the css class to
|
||||
* @param cssClassName CSS class name
|
||||
*/
|
||||
setTextStyle(itemId, cssClassName) {
|
||||
const item = this.getState(itemId);
|
||||
if (item !== undefined) {
|
||||
item.textStyles.push(cssClassName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the direction statement in the root document.
|
||||
* @private
|
||||
* @returns {{ value: string } | undefined} - the direction statement if present
|
||||
*/
|
||||
getDirectionStatement() {
|
||||
return this.rootDoc.find((doc) => doc.stmt === STMT_DIRECTION);
|
||||
}
|
||||
|
||||
getDirection() {
|
||||
return this.getDirectionStatement()?.value ?? DEFAULT_DIAGRAM_DIRECTION;
|
||||
}
|
||||
|
||||
setDirection(dir) {
|
||||
const doc = this.getDirectionStatement();
|
||||
if (doc) {
|
||||
doc.value = dir;
|
||||
} else {
|
||||
this.rootDoc.unshift({ stmt: STMT_DIRECTION, value: dir });
|
||||
}
|
||||
}
|
||||
|
||||
trimColon(str) {
|
||||
return str && str[0] === ':' ? str.substr(1).trim() : str.trim();
|
||||
}
|
||||
|
||||
getData() {
|
||||
const config = getConfig();
|
||||
return {
|
||||
nodes: this.nodes,
|
||||
edges: this.edges,
|
||||
other: {},
|
||||
config,
|
||||
direction: getDir(this.getRootDocV2()),
|
||||
};
|
||||
}
|
||||
|
||||
getConfig() {
|
||||
return getConfig().state;
|
||||
}
|
||||
getAccTitle = getAccTitle;
|
||||
setAccTitle = setAccTitle;
|
||||
getAccDescription = getAccDescription;
|
||||
setAccDescription = setAccDescription;
|
||||
setDiagramTitle = setDiagramTitle;
|
||||
getDiagramTitle = getDiagramTitle;
|
||||
}
|
||||
693
packages/mermaid/src/diagrams/state/stateDb.ts
Normal file
693
packages/mermaid/src/diagrams/state/stateDb.ts
Normal file
@@ -0,0 +1,693 @@
|
||||
import { getConfig } from '../../diagram-api/diagramAPI.js';
|
||||
import { log } from '../../logger.js';
|
||||
import { generateId } from '../../utils.js';
|
||||
import common from '../common/common.js';
|
||||
import {
|
||||
clear as commonClear,
|
||||
getAccDescription,
|
||||
getAccTitle,
|
||||
getDiagramTitle,
|
||||
setAccDescription,
|
||||
setAccTitle,
|
||||
setDiagramTitle,
|
||||
} from '../common/commonDb.js';
|
||||
import { dataFetcher, reset as resetDataFetcher } from './dataFetcher.js';
|
||||
import { getDir } from './stateRenderer-v3-unified.js';
|
||||
import {
|
||||
DEFAULT_DIAGRAM_DIRECTION,
|
||||
DEFAULT_STATE_TYPE,
|
||||
DIVIDER_TYPE,
|
||||
STMT_APPLYCLASS,
|
||||
STMT_CLASSDEF,
|
||||
STMT_RELATION,
|
||||
STMT_ROOT,
|
||||
STMT_DIRECTION,
|
||||
STMT_STATE,
|
||||
STMT_STYLEDEF,
|
||||
} from './stateCommon.js';
|
||||
import type { MermaidConfig } from '../../config.type.js';
|
||||
|
||||
const CONSTANTS = {
|
||||
START_NODE: '[*]',
|
||||
START_TYPE: 'start',
|
||||
END_NODE: '[*]',
|
||||
END_TYPE: 'end',
|
||||
COLOR_KEYWORD: 'color',
|
||||
FILL_KEYWORD: 'fill',
|
||||
BG_FILL: 'bgFill',
|
||||
STYLECLASS_SEP: ',',
|
||||
} as const;
|
||||
|
||||
interface BaseStmt {
|
||||
stmt: 'applyClass' | 'classDef' | 'dir' | 'relation' | 'state' | 'style' | 'root' | 'default';
|
||||
}
|
||||
|
||||
interface ApplyClassStmt extends BaseStmt {
|
||||
stmt: 'applyClass';
|
||||
id: string;
|
||||
styleClass: string;
|
||||
}
|
||||
|
||||
interface ClassDefStmt extends BaseStmt {
|
||||
stmt: 'classDef';
|
||||
id: string;
|
||||
classes: string;
|
||||
}
|
||||
|
||||
interface DirectionStmt extends BaseStmt {
|
||||
stmt: 'dir';
|
||||
value: 'TB' | 'BT' | 'RL' | 'LR';
|
||||
}
|
||||
|
||||
interface RelationStmt extends BaseStmt {
|
||||
stmt: 'relation';
|
||||
state1: StateStmt;
|
||||
state2: StateStmt;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface StateStmt extends BaseStmt {
|
||||
stmt: 'state' | 'default';
|
||||
id: string;
|
||||
type: 'default' | 'fork' | 'join' | 'choice' | 'divider' | 'start' | 'end';
|
||||
description?: string;
|
||||
descriptions?: string[];
|
||||
doc?: Stmt[];
|
||||
note?: Note;
|
||||
start?: boolean;
|
||||
classes?: string[];
|
||||
styles?: string[];
|
||||
textStyles?: string[];
|
||||
}
|
||||
|
||||
interface StyleStmt extends BaseStmt {
|
||||
stmt: 'style';
|
||||
id: string;
|
||||
styleClass: string;
|
||||
}
|
||||
|
||||
export interface RootStmt {
|
||||
id: 'root';
|
||||
stmt: 'root';
|
||||
doc?: Stmt[];
|
||||
}
|
||||
|
||||
interface Note {
|
||||
position?: 'left of' | 'right of';
|
||||
text: string;
|
||||
}
|
||||
|
||||
export type Stmt =
|
||||
| ApplyClassStmt
|
||||
| ClassDefStmt
|
||||
| DirectionStmt
|
||||
| RelationStmt
|
||||
| StateStmt
|
||||
| StyleStmt
|
||||
| RootStmt;
|
||||
|
||||
interface DiagramEdge {
|
||||
id1: string;
|
||||
id2: string;
|
||||
relationTitle?: string;
|
||||
}
|
||||
|
||||
interface Document {
|
||||
relations: DiagramEdge[];
|
||||
states: Map<string, StateStmt>;
|
||||
documents: Record<string, Document>;
|
||||
}
|
||||
|
||||
export interface StyleClass {
|
||||
id: string;
|
||||
styles: string[];
|
||||
textStyles: string[];
|
||||
}
|
||||
|
||||
export interface NodeData {
|
||||
labelStyle?: string;
|
||||
shape: string;
|
||||
label?: string | string[];
|
||||
cssClasses: string;
|
||||
cssCompiledStyles?: string[];
|
||||
cssStyles: string[];
|
||||
id: string;
|
||||
dir?: string;
|
||||
domId?: string;
|
||||
type?: string;
|
||||
isGroup?: boolean;
|
||||
padding?: number;
|
||||
rx?: number;
|
||||
ry?: number;
|
||||
look?: MermaidConfig['look'];
|
||||
parentId?: string;
|
||||
centerLabel?: boolean;
|
||||
position?: string;
|
||||
description?: string | string[];
|
||||
}
|
||||
|
||||
export interface Edge {
|
||||
id: string;
|
||||
start: string;
|
||||
end: string;
|
||||
arrowhead: string;
|
||||
arrowTypeEnd: string;
|
||||
style: string;
|
||||
labelStyle: string;
|
||||
label?: string;
|
||||
arrowheadStyle: string;
|
||||
labelpos: string;
|
||||
labelType: string;
|
||||
thickness: string;
|
||||
classes: string;
|
||||
look: MermaidConfig['look'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new list of classes.
|
||||
* In the future, this can be replaced with a class common to all diagrams.
|
||||
* ClassDef information = \{ id: id, styles: [], textStyles: [] \}
|
||||
*/
|
||||
const newClassesList = (): Map<string, StyleClass> => new Map();
|
||||
const newDoc = (): Document => ({
|
||||
relations: [],
|
||||
states: new Map(),
|
||||
documents: {},
|
||||
});
|
||||
const clone = <T>(o: T): T => JSON.parse(JSON.stringify(o));
|
||||
|
||||
export class StateDB {
|
||||
private nodes: NodeData[] = [];
|
||||
private edges: Edge[] = [];
|
||||
private rootDoc: Stmt[] = [];
|
||||
private classes = newClassesList();
|
||||
private documents = { root: newDoc() };
|
||||
private currentDocument = this.documents.root;
|
||||
private startEndCount = 0;
|
||||
private dividerCnt = 0;
|
||||
|
||||
static readonly relationType = {
|
||||
AGGREGATION: 0,
|
||||
EXTENSION: 1,
|
||||
COMPOSITION: 2,
|
||||
DEPENDENCY: 3,
|
||||
} as const;
|
||||
|
||||
constructor(private version: 1 | 2) {
|
||||
this.clear();
|
||||
// Bind methods used by JISON
|
||||
this.setRootDoc = this.setRootDoc.bind(this);
|
||||
this.getDividerId = this.getDividerId.bind(this);
|
||||
this.setDirection = this.setDirection.bind(this);
|
||||
this.trimColon = this.trimColon.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert all of the statements (stmts) that were parsed into states and relationships.
|
||||
* This is done because a state diagram may have nested sections,
|
||||
* where each section is a 'document' and has its own set of statements.
|
||||
* Ex: the section within a fork has its own statements, and incoming and outgoing statements
|
||||
* refer to the fork as a whole (document).
|
||||
* See the parser grammar: the definition of a document is a document then a 'line', where a line can be a statement.
|
||||
* This will push the statement into the list of statements for the current document.
|
||||
*/
|
||||
extract(statements: Stmt[] | { doc: Stmt[] }) {
|
||||
this.clear(true);
|
||||
for (const item of Array.isArray(statements) ? statements : statements.doc) {
|
||||
switch (item.stmt) {
|
||||
case STMT_STATE:
|
||||
this.addState(item.id.trim(), item.type, item.doc, item.description, item.note);
|
||||
break;
|
||||
case STMT_RELATION:
|
||||
this.addRelation(item.state1, item.state2, item.description);
|
||||
break;
|
||||
case STMT_CLASSDEF:
|
||||
this.addStyleClass(item.id.trim(), item.classes);
|
||||
break;
|
||||
case STMT_STYLEDEF:
|
||||
this.handleStyleDef(item);
|
||||
break;
|
||||
case STMT_APPLYCLASS:
|
||||
this.setCssClass(item.id.trim(), item.styleClass);
|
||||
break;
|
||||
}
|
||||
}
|
||||
const diagramStates = this.getStates();
|
||||
const config = getConfig();
|
||||
|
||||
resetDataFetcher();
|
||||
dataFetcher(
|
||||
undefined,
|
||||
this.getRootDocV2() as StateStmt,
|
||||
diagramStates,
|
||||
this.nodes,
|
||||
this.edges,
|
||||
true,
|
||||
config.look,
|
||||
this.classes
|
||||
);
|
||||
|
||||
// Process node labels
|
||||
for (const node of this.nodes) {
|
||||
if (!Array.isArray(node.label)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
node.description = node.label.slice(1);
|
||||
if (node.isGroup && node.description.length > 0) {
|
||||
throw new Error(
|
||||
`Group nodes can only have label. Remove the additional description for node [${node.id}]`
|
||||
);
|
||||
}
|
||||
node.label = node.label[0];
|
||||
}
|
||||
}
|
||||
|
||||
private handleStyleDef(item: StyleStmt) {
|
||||
const ids = item.id.trim().split(',');
|
||||
const styles = item.styleClass.split(',');
|
||||
|
||||
for (const id of ids) {
|
||||
let state = this.getState(id);
|
||||
if (!state) {
|
||||
const trimmedId = id.trim();
|
||||
this.addState(trimmedId);
|
||||
state = this.getState(trimmedId);
|
||||
}
|
||||
if (state) {
|
||||
state.styles = styles.map((s) => s.replace(/;/g, '')?.trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setRootDoc(o: Stmt[]) {
|
||||
log.info('Setting root doc', o);
|
||||
this.rootDoc = o;
|
||||
if (this.version === 1) {
|
||||
this.extract(o);
|
||||
} else {
|
||||
this.extract(this.getRootDocV2());
|
||||
}
|
||||
}
|
||||
|
||||
docTranslator(parent: RootStmt | StateStmt, node: Stmt, first: boolean) {
|
||||
if (node.stmt === STMT_RELATION) {
|
||||
this.docTranslator(parent, node.state1, true);
|
||||
this.docTranslator(parent, node.state2, false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.stmt === STMT_STATE) {
|
||||
if (node.id === CONSTANTS.START_NODE) {
|
||||
node.id = parent.id + (first ? '_start' : '_end');
|
||||
node.start = first;
|
||||
} else {
|
||||
// This is just a plain state, not a start or end
|
||||
node.id = node.id.trim();
|
||||
}
|
||||
}
|
||||
|
||||
if ((node.stmt !== STMT_ROOT && node.stmt !== STMT_STATE) || !node.doc) {
|
||||
return;
|
||||
}
|
||||
|
||||
const doc = [];
|
||||
// Check for concurrency
|
||||
let currentDoc = [];
|
||||
for (const stmt of node.doc) {
|
||||
if ((stmt as StateStmt).type === DIVIDER_TYPE) {
|
||||
const newNode = clone(stmt as StateStmt);
|
||||
newNode.doc = clone(currentDoc);
|
||||
doc.push(newNode);
|
||||
currentDoc = [];
|
||||
} else {
|
||||
currentDoc.push(stmt);
|
||||
}
|
||||
}
|
||||
|
||||
// If any divider was encountered
|
||||
if (doc.length > 0 && currentDoc.length > 0) {
|
||||
const newNode = {
|
||||
stmt: STMT_STATE,
|
||||
id: generateId(),
|
||||
type: 'divider',
|
||||
doc: clone(currentDoc),
|
||||
} satisfies StateStmt;
|
||||
doc.push(clone(newNode));
|
||||
node.doc = doc;
|
||||
}
|
||||
|
||||
node.doc.forEach((docNode) => this.docTranslator(node, docNode, true));
|
||||
}
|
||||
|
||||
private getRootDocV2() {
|
||||
this.docTranslator(
|
||||
{ id: STMT_ROOT, stmt: STMT_ROOT },
|
||||
{ id: STMT_ROOT, stmt: STMT_ROOT, doc: this.rootDoc },
|
||||
true
|
||||
);
|
||||
return { id: STMT_ROOT, doc: this.rootDoc };
|
||||
}
|
||||
|
||||
/**
|
||||
* Function called by parser when a node definition has been found.
|
||||
*
|
||||
* @param descr - description for the state. Can be a string or a list or strings
|
||||
* @param classes - class styles to apply to this state. Can be a string (1 style) or an array of styles. If it's just 1 class, convert it to an array of that 1 class.
|
||||
* @param styles - styles to apply to this state. Can be a string (1 style) or an array of styles. If it's just 1 style, convert it to an array of that 1 style.
|
||||
* @param textStyles - text styles to apply to this state. Can be a string (1 text test) or an array of text styles. If it's just 1 text style, convert it to an array of that 1 text style.
|
||||
*/
|
||||
addState(
|
||||
id: string,
|
||||
type: StateStmt['type'] = DEFAULT_STATE_TYPE,
|
||||
doc: Stmt[] | undefined = undefined,
|
||||
descr: string | string[] | undefined = undefined,
|
||||
note: Note | undefined = undefined,
|
||||
classes: string | string[] | undefined = undefined,
|
||||
styles: string | string[] | undefined = undefined,
|
||||
textStyles: string | string[] | undefined = undefined
|
||||
) {
|
||||
const trimmedId = id?.trim();
|
||||
if (!this.currentDocument.states.has(trimmedId)) {
|
||||
log.info('Adding state ', trimmedId, descr);
|
||||
this.currentDocument.states.set(trimmedId, {
|
||||
stmt: STMT_STATE,
|
||||
id: trimmedId,
|
||||
descriptions: [],
|
||||
type,
|
||||
doc,
|
||||
note,
|
||||
classes: [],
|
||||
styles: [],
|
||||
textStyles: [],
|
||||
});
|
||||
} else {
|
||||
const state = this.currentDocument.states.get(trimmedId);
|
||||
if (!state) {
|
||||
throw new Error(`State not found: ${trimmedId}`);
|
||||
}
|
||||
if (!state.doc) {
|
||||
state.doc = doc;
|
||||
}
|
||||
if (!state.type) {
|
||||
state.type = type;
|
||||
}
|
||||
}
|
||||
|
||||
if (descr) {
|
||||
log.info('Setting state description', trimmedId, descr);
|
||||
const descriptions = Array.isArray(descr) ? descr : [descr];
|
||||
descriptions.forEach((des) => this.addDescription(trimmedId, des.trim()));
|
||||
}
|
||||
|
||||
if (note) {
|
||||
const doc2 = this.currentDocument.states.get(trimmedId);
|
||||
if (!doc2) {
|
||||
throw new Error(`State not found: ${trimmedId}`);
|
||||
}
|
||||
doc2.note = note;
|
||||
doc2.note.text = common.sanitizeText(doc2.note.text, getConfig());
|
||||
}
|
||||
|
||||
if (classes) {
|
||||
log.info('Setting state classes', trimmedId, classes);
|
||||
const classesList = Array.isArray(classes) ? classes : [classes];
|
||||
classesList.forEach((cssClass) => this.setCssClass(trimmedId, cssClass.trim()));
|
||||
}
|
||||
|
||||
if (styles) {
|
||||
log.info('Setting state styles', trimmedId, styles);
|
||||
const stylesList = Array.isArray(styles) ? styles : [styles];
|
||||
stylesList.forEach((style) => this.setStyle(trimmedId, style.trim()));
|
||||
}
|
||||
|
||||
if (textStyles) {
|
||||
log.info('Setting state styles', trimmedId, styles);
|
||||
const textStylesList = Array.isArray(textStyles) ? textStyles : [textStyles];
|
||||
textStylesList.forEach((textStyle) => this.setTextStyle(trimmedId, textStyle.trim()));
|
||||
}
|
||||
}
|
||||
|
||||
clear(saveCommon?: boolean) {
|
||||
this.nodes = [];
|
||||
this.edges = [];
|
||||
this.documents = { root: newDoc() };
|
||||
this.currentDocument = this.documents.root;
|
||||
|
||||
// number of start and end nodes; used to construct ids
|
||||
this.startEndCount = 0;
|
||||
this.classes = newClassesList();
|
||||
if (!saveCommon) {
|
||||
commonClear();
|
||||
}
|
||||
}
|
||||
|
||||
getState(id: string) {
|
||||
return this.currentDocument.states.get(id);
|
||||
}
|
||||
|
||||
getStates() {
|
||||
return this.currentDocument.states;
|
||||
}
|
||||
|
||||
logDocuments() {
|
||||
log.info('Documents = ', this.documents);
|
||||
}
|
||||
|
||||
getRelations() {
|
||||
return this.currentDocument.relations;
|
||||
}
|
||||
|
||||
/**
|
||||
* If the id is a start node ( [*] ), then return a new id constructed from
|
||||
* the start node name and the current start node count.
|
||||
* else return the given id
|
||||
*/
|
||||
startIdIfNeeded(id = '') {
|
||||
if (id === CONSTANTS.START_NODE) {
|
||||
this.startEndCount++;
|
||||
return `${CONSTANTS.START_TYPE}${this.startEndCount}`;
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* If the id is a start node ( [*] ), then return the start type ('start')
|
||||
* else return the given type
|
||||
*/
|
||||
startTypeIfNeeded(id = '', type: StateStmt['type'] = DEFAULT_STATE_TYPE) {
|
||||
return id === CONSTANTS.START_NODE ? CONSTANTS.START_TYPE : type;
|
||||
}
|
||||
|
||||
/**
|
||||
* If the id is an end node ( [*] ), then return a new id constructed from
|
||||
* the end node name and the current start_end node count.
|
||||
* else return the given id
|
||||
*/
|
||||
endIdIfNeeded(id = '') {
|
||||
if (id === CONSTANTS.END_NODE) {
|
||||
this.startEndCount++;
|
||||
return `${CONSTANTS.END_TYPE}${this.startEndCount}`;
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* If the id is an end node ( [*] ), then return the end type
|
||||
* else return the given type
|
||||
*
|
||||
*/
|
||||
endTypeIfNeeded(id = '', type: StateStmt['type'] = DEFAULT_STATE_TYPE) {
|
||||
return id === CONSTANTS.END_NODE ? CONSTANTS.END_TYPE : type;
|
||||
}
|
||||
|
||||
addRelationObjs(item1: StateStmt, item2: StateStmt, relationTitle = '') {
|
||||
const id1 = this.startIdIfNeeded(item1.id.trim());
|
||||
const type1 = this.startTypeIfNeeded(item1.id.trim(), item1.type);
|
||||
const id2 = this.startIdIfNeeded(item2.id.trim());
|
||||
const type2 = this.startTypeIfNeeded(item2.id.trim(), item2.type);
|
||||
this.addState(
|
||||
id1,
|
||||
type1,
|
||||
item1.doc,
|
||||
item1.description,
|
||||
item1.note,
|
||||
item1.classes,
|
||||
item1.styles,
|
||||
item1.textStyles
|
||||
);
|
||||
this.addState(
|
||||
id2,
|
||||
type2,
|
||||
item2.doc,
|
||||
item2.description,
|
||||
item2.note,
|
||||
item2.classes,
|
||||
item2.styles,
|
||||
item2.textStyles
|
||||
);
|
||||
this.currentDocument.relations.push({
|
||||
id1,
|
||||
id2,
|
||||
relationTitle: common.sanitizeText(relationTitle, getConfig()),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a relation between two items. The items may be full objects or just the string id of a state.
|
||||
*/
|
||||
addRelation(item1: string | StateStmt, item2: string | StateStmt, title?: string) {
|
||||
if (typeof item1 === 'object' && typeof item2 === 'object') {
|
||||
this.addRelationObjs(item1, item2, title);
|
||||
} else if (typeof item1 === 'string' && typeof item2 === 'string') {
|
||||
const id1 = this.startIdIfNeeded(item1.trim());
|
||||
const type1 = this.startTypeIfNeeded(item1);
|
||||
const id2 = this.endIdIfNeeded(item2.trim());
|
||||
const type2 = this.endTypeIfNeeded(item2);
|
||||
|
||||
this.addState(id1, type1);
|
||||
this.addState(id2, type2);
|
||||
this.currentDocument.relations.push({
|
||||
id1,
|
||||
id2,
|
||||
relationTitle: title ? common.sanitizeText(title, getConfig()) : undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
addDescription(id: string, descr: string) {
|
||||
const theState = this.currentDocument.states.get(id);
|
||||
const _descr = descr.startsWith(':') ? descr.replace(':', '').trim() : descr;
|
||||
theState?.descriptions?.push(common.sanitizeText(_descr, getConfig()));
|
||||
}
|
||||
|
||||
cleanupLabel(label: string) {
|
||||
return label.startsWith(':') ? label.slice(2).trim() : label.trim();
|
||||
}
|
||||
|
||||
getDividerId() {
|
||||
this.dividerCnt++;
|
||||
return `divider-id-${this.dividerCnt}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the parser comes across a (style) class definition
|
||||
* @example classDef my-style fill:#f96;
|
||||
*
|
||||
* @param id - the id of this (style) class
|
||||
* @param styleAttributes - the string with 1 or more style attributes (each separated by a comma)
|
||||
*/
|
||||
addStyleClass(id: string, styleAttributes = '') {
|
||||
// create a new style class object with this id
|
||||
if (!this.classes.has(id)) {
|
||||
this.classes.set(id, { id, styles: [], textStyles: [] });
|
||||
}
|
||||
const foundClass = this.classes.get(id);
|
||||
if (styleAttributes && foundClass) {
|
||||
styleAttributes.split(CONSTANTS.STYLECLASS_SEP).forEach((attrib) => {
|
||||
const fixedAttrib = attrib.replace(/([^;]*);/, '$1').trim();
|
||||
if (RegExp(CONSTANTS.COLOR_KEYWORD).exec(attrib)) {
|
||||
const newStyle1 = fixedAttrib.replace(CONSTANTS.FILL_KEYWORD, CONSTANTS.BG_FILL);
|
||||
const newStyle2 = newStyle1.replace(CONSTANTS.COLOR_KEYWORD, CONSTANTS.FILL_KEYWORD);
|
||||
foundClass.textStyles.push(newStyle2);
|
||||
}
|
||||
foundClass.styles.push(fixedAttrib);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getClasses() {
|
||||
return this.classes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a (style) class or css class to a state with the given id.
|
||||
* If the state isn't already in the list of known states, add it.
|
||||
* Might be called by parser when a style class or CSS class should be applied to a state
|
||||
*
|
||||
* @param itemIds - The id or a list of ids of the item(s) to apply the css class to
|
||||
* @param cssClassName - CSS class name
|
||||
*/
|
||||
setCssClass(itemIds: string, cssClassName: string) {
|
||||
itemIds.split(',').forEach((id) => {
|
||||
let foundState = this.getState(id);
|
||||
if (!foundState) {
|
||||
const trimmedId = id.trim();
|
||||
this.addState(trimmedId);
|
||||
foundState = this.getState(trimmedId);
|
||||
}
|
||||
foundState?.classes?.push(cssClassName);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a style to a state with the given id.
|
||||
* @example style stateId fill:#f9f,stroke:#333,stroke-width:4px
|
||||
* where 'style' is the keyword
|
||||
* stateId is the id of a state
|
||||
* the rest of the string is the styleText (all of the attributes to be applied to the state)
|
||||
*
|
||||
* @param itemId - The id of item to apply the style to
|
||||
* @param styleText - the text of the attributes for the style
|
||||
*/
|
||||
setStyle(itemId: string, styleText: string) {
|
||||
this.getState(itemId)?.styles?.push(styleText);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a text style to a state with the given id
|
||||
*
|
||||
* @param itemId - The id of item to apply the css class to
|
||||
* @param cssClassName - CSS class name
|
||||
*/
|
||||
setTextStyle(itemId: string, cssClassName: string) {
|
||||
this.getState(itemId)?.textStyles?.push(cssClassName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the direction statement in the root document.
|
||||
* @returns the direction statement if present
|
||||
*/
|
||||
private getDirectionStatement() {
|
||||
return this.rootDoc.find((doc): doc is DirectionStmt => doc.stmt === STMT_DIRECTION);
|
||||
}
|
||||
|
||||
getDirection() {
|
||||
return this.getDirectionStatement()?.value ?? DEFAULT_DIAGRAM_DIRECTION;
|
||||
}
|
||||
|
||||
setDirection(dir: DirectionStmt['value']) {
|
||||
const doc = this.getDirectionStatement();
|
||||
if (doc) {
|
||||
doc.value = dir;
|
||||
} else {
|
||||
this.rootDoc.unshift({ stmt: STMT_DIRECTION, value: dir });
|
||||
}
|
||||
}
|
||||
|
||||
trimColon(str: string) {
|
||||
return str.startsWith(':') ? str.slice(1).trim() : str.trim();
|
||||
}
|
||||
|
||||
getData() {
|
||||
const config = getConfig();
|
||||
return {
|
||||
nodes: this.nodes,
|
||||
edges: this.edges,
|
||||
other: {},
|
||||
config,
|
||||
direction: getDir(this.getRootDocV2()),
|
||||
};
|
||||
}
|
||||
|
||||
getConfig() {
|
||||
return getConfig().state;
|
||||
}
|
||||
|
||||
getAccTitle = getAccTitle;
|
||||
setAccTitle = setAccTitle;
|
||||
getAccDescription = getAccDescription;
|
||||
setAccDescription = setAccDescription;
|
||||
setDiagramTitle = setDiagramTitle;
|
||||
getDiagramTitle = getDiagramTitle;
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { StateDB } from './stateDb.js';
|
||||
describe('state diagram V2, ', function () {
|
||||
// TODO - these examples should be put into ./parser/stateDiagram.spec.js
|
||||
describe('when parsing an info graph it', function () {
|
||||
/** @type {StateDB} */
|
||||
let stateDb;
|
||||
beforeEach(function () {
|
||||
stateDb = new StateDB(2);
|
||||
@@ -347,6 +348,20 @@ describe('state diagram V2, ', function () {
|
||||
`;
|
||||
|
||||
parser.parse(str);
|
||||
expect(stateDb.getState('Active').note).toMatchInlineSnapshot(`
|
||||
{
|
||||
"position": "left of",
|
||||
"text": "this is a short<br>note",
|
||||
}
|
||||
`);
|
||||
expect(stateDb.getState('Inactive').note).toMatchInlineSnapshot(`
|
||||
{
|
||||
"position": "right of",
|
||||
"text": "A note can also
|
||||
be defined on
|
||||
several lines",
|
||||
}
|
||||
`);
|
||||
});
|
||||
it('should handle multiline notes with different line breaks', function () {
|
||||
const str = `stateDiagram-v2
|
||||
@@ -357,6 +372,12 @@ describe('state diagram V2, ', function () {
|
||||
`;
|
||||
|
||||
parser.parse(str);
|
||||
expect(stateDb.getStates().get('State1').note).toMatchInlineSnapshot(`
|
||||
{
|
||||
"position": "right of",
|
||||
"text": "Line1<br>Line2<br>Line3<br>Line4<br>Line5",
|
||||
}
|
||||
`);
|
||||
});
|
||||
it('should handle floating notes', function () {
|
||||
const str = `stateDiagram-v2
|
||||
@@ -367,15 +388,14 @@ describe('state diagram V2, ', function () {
|
||||
parser.parse(str);
|
||||
});
|
||||
it('should handle floating notes', function () {
|
||||
const str = `stateDiagram-v2\n
|
||||
const str = `stateDiagram-v2
|
||||
state foo
|
||||
note "This is a floating note" as N1
|
||||
`;
|
||||
|
||||
parser.parse(str);
|
||||
});
|
||||
it('should handle notes for composite (nested) states', function () {
|
||||
const str = `stateDiagram-v2\n
|
||||
const str = `stateDiagram-v2
|
||||
[*] --> NotShooting
|
||||
|
||||
state "Not Shooting State" as NotShooting {
|
||||
@@ -390,6 +410,12 @@ describe('state diagram V2, ', function () {
|
||||
`;
|
||||
|
||||
parser.parse(str);
|
||||
expect(stateDb.getState('NotShooting').note).toMatchInlineSnapshot(`
|
||||
{
|
||||
"position": "right of",
|
||||
"text": "This is a note on a composite state",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('A composite state should be able to link to itself', () => {
|
||||
|
||||
@@ -13,15 +13,17 @@ export const setConf = function (cnf) {
|
||||
};
|
||||
|
||||
const actors = {};
|
||||
let maxWidth = 0;
|
||||
|
||||
/** @param diagram - The diagram to draw to. */
|
||||
function drawActorLegend(diagram) {
|
||||
const conf = getConfig().journey;
|
||||
// Draw the actors
|
||||
const maxLabelWidth = conf.maxLabelWidth;
|
||||
maxWidth = 0;
|
||||
let yPos = 60;
|
||||
|
||||
Object.keys(actors).forEach((person) => {
|
||||
const colour = actors[person].color;
|
||||
|
||||
const circleData = {
|
||||
cx: 20,
|
||||
cy: yPos,
|
||||
@@ -32,25 +34,97 @@ function drawActorLegend(diagram) {
|
||||
};
|
||||
svgDraw.drawCircle(diagram, circleData);
|
||||
|
||||
const labelData = {
|
||||
x: 40,
|
||||
y: yPos + 7,
|
||||
fill: '#666',
|
||||
text: person,
|
||||
textMargin: conf.boxTextMargin | 5,
|
||||
};
|
||||
svgDraw.drawText(diagram, labelData);
|
||||
// First, measure the full text width without wrapping.
|
||||
let measureText = diagram.append('text').attr('visibility', 'hidden').text(person);
|
||||
const fullTextWidth = measureText.node().getBoundingClientRect().width;
|
||||
measureText.remove();
|
||||
|
||||
yPos += 20;
|
||||
let lines = [];
|
||||
|
||||
// If the text is naturally within the max width, use it as a single line.
|
||||
if (fullTextWidth <= maxLabelWidth) {
|
||||
lines = [person];
|
||||
} else {
|
||||
// Otherwise, wrap the text using the knuth-plass algorithm.
|
||||
const words = person.split(' '); // Split the text into words.
|
||||
let currentLine = '';
|
||||
measureText = diagram.append('text').attr('visibility', 'hidden');
|
||||
|
||||
words.forEach((word) => {
|
||||
// check the width of the line with the new word.
|
||||
const testLine = currentLine ? `${currentLine} ${word}` : word;
|
||||
measureText.text(testLine);
|
||||
const textWidth = measureText.node().getBoundingClientRect().width;
|
||||
|
||||
if (textWidth > maxLabelWidth) {
|
||||
// If adding the new word exceeds max width, push the current line.
|
||||
if (currentLine) {
|
||||
lines.push(currentLine);
|
||||
}
|
||||
currentLine = word; // Start a new line with the current word.
|
||||
|
||||
// If the word itself is too long, break it with a hyphen.
|
||||
measureText.text(word);
|
||||
if (measureText.node().getBoundingClientRect().width > maxLabelWidth) {
|
||||
let brokenWord = '';
|
||||
for (const char of word) {
|
||||
brokenWord += char;
|
||||
measureText.text(brokenWord + '-');
|
||||
if (measureText.node().getBoundingClientRect().width > maxLabelWidth) {
|
||||
// Push the broken part with a hyphen.
|
||||
lines.push(brokenWord.slice(0, -1) + '-');
|
||||
brokenWord = char;
|
||||
}
|
||||
}
|
||||
currentLine = brokenWord;
|
||||
}
|
||||
} else {
|
||||
// If the line with the new word fits, add the new word to the current line.
|
||||
currentLine = testLine;
|
||||
}
|
||||
});
|
||||
|
||||
// Push the last line.
|
||||
if (currentLine) {
|
||||
lines.push(currentLine);
|
||||
}
|
||||
measureText.remove(); // Remove the text element used for measuring.
|
||||
}
|
||||
|
||||
lines.forEach((line, index) => {
|
||||
const labelData = {
|
||||
x: 40,
|
||||
y: yPos + 7 + index * 20,
|
||||
fill: '#666',
|
||||
text: line,
|
||||
textMargin: conf.boxTextMargin ?? 5,
|
||||
};
|
||||
|
||||
// Draw the text and measure the width.
|
||||
const textElement = svgDraw.drawText(diagram, labelData);
|
||||
const lineWidth = textElement.node().getBoundingClientRect().width;
|
||||
|
||||
// Use conf.leftMargin as the initial spacing baseline,
|
||||
// but expand maxWidth if the line is wider.
|
||||
if (lineWidth > maxWidth && lineWidth > conf.leftMargin - lineWidth) {
|
||||
maxWidth = lineWidth;
|
||||
}
|
||||
});
|
||||
|
||||
yPos += Math.max(20, lines.length * 20);
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: Cleanup?
|
||||
const conf = getConfig().journey;
|
||||
const LEFT_MARGIN = conf.leftMargin;
|
||||
let leftMargin = 0;
|
||||
export const draw = function (text, id, version, diagObj) {
|
||||
const conf = getConfig().journey;
|
||||
const configObject = getConfig();
|
||||
const titleColor = configObject.journey.titleColor;
|
||||
const titleFontSize = configObject.journey.titleFontSize;
|
||||
const titleFontFamily = configObject.journey.titleFontFamily;
|
||||
|
||||
const securityLevel = getConfig().securityLevel;
|
||||
const securityLevel = configObject.securityLevel;
|
||||
// Handle root and Document for when rendering in sandbox mode
|
||||
let sandboxElement;
|
||||
if (securityLevel === 'sandbox') {
|
||||
@@ -84,7 +158,8 @@ export const draw = function (text, id, version, diagObj) {
|
||||
});
|
||||
|
||||
drawActorLegend(diagram);
|
||||
bounds.insert(0, 0, LEFT_MARGIN, Object.keys(actors).length * 50);
|
||||
leftMargin = conf.leftMargin + maxWidth;
|
||||
bounds.insert(0, 0, leftMargin, Object.keys(actors).length * 50);
|
||||
drawTasks(diagram, tasks, 0);
|
||||
|
||||
const box = bounds.getBounds();
|
||||
@@ -92,23 +167,25 @@ export const draw = function (text, id, version, diagObj) {
|
||||
diagram
|
||||
.append('text')
|
||||
.text(title)
|
||||
.attr('x', LEFT_MARGIN)
|
||||
.attr('font-size', '4ex')
|
||||
.attr('x', leftMargin)
|
||||
.attr('font-size', titleFontSize)
|
||||
.attr('font-weight', 'bold')
|
||||
.attr('y', 25);
|
||||
.attr('y', 25)
|
||||
.attr('fill', titleColor)
|
||||
.attr('font-family', titleFontFamily);
|
||||
}
|
||||
|
||||
const height = box.stopy - box.starty + 2 * conf.diagramMarginY;
|
||||
const width = LEFT_MARGIN + box.stopx + 2 * conf.diagramMarginX;
|
||||
const width = leftMargin + box.stopx + 2 * conf.diagramMarginX;
|
||||
|
||||
configureSvgSize(diagram, height, width, conf.useMaxWidth);
|
||||
|
||||
// Draw activity line
|
||||
diagram
|
||||
.append('line')
|
||||
.attr('x1', LEFT_MARGIN)
|
||||
.attr('x1', leftMargin)
|
||||
.attr('y1', conf.height * 4) // One section head + one task + margins
|
||||
.attr('x2', width - LEFT_MARGIN - 4) // Subtract stroke width so arrow point is retained
|
||||
.attr('x2', width - leftMargin - 4) // Subtract stroke width so arrow point is retained
|
||||
.attr('y2', conf.height * 4)
|
||||
.attr('stroke-width', 4)
|
||||
.attr('stroke', 'black')
|
||||
@@ -234,7 +311,7 @@ export const drawTasks = function (diagram, tasks, verticalPos) {
|
||||
}
|
||||
|
||||
const section = {
|
||||
x: i * conf.taskMargin + i * conf.width + LEFT_MARGIN,
|
||||
x: i * conf.taskMargin + i * conf.width + leftMargin,
|
||||
y: 50,
|
||||
text: task.section,
|
||||
fill,
|
||||
@@ -258,7 +335,7 @@ export const drawTasks = function (diagram, tasks, verticalPos) {
|
||||
}, {});
|
||||
|
||||
// Add some rendering data to the object
|
||||
task.x = i * conf.taskMargin + i * conf.width + LEFT_MARGIN;
|
||||
task.x = i * conf.taskMargin + i * conf.width + leftMargin;
|
||||
task.y = taskPos;
|
||||
task.width = conf.diagramMarginX;
|
||||
task.height = conf.diagramMarginY;
|
||||
|
||||
@@ -93,6 +93,7 @@ export interface XYChartConfig {
|
||||
titleFontSize: number;
|
||||
titlePadding: number;
|
||||
showTitle: boolean;
|
||||
showDataLabel: boolean;
|
||||
xAxis: XYChartAxisConfig;
|
||||
yAxis: XYChartAxisConfig;
|
||||
chartOrientation: 'vertical' | 'horizontal';
|
||||
|
||||
@@ -195,6 +195,10 @@ function getChartConfig() {
|
||||
return xyChartConfig;
|
||||
}
|
||||
|
||||
function getXYChartData() {
|
||||
return xyChartData;
|
||||
}
|
||||
|
||||
const clear = function () {
|
||||
commonClear();
|
||||
plotIndex = 0;
|
||||
@@ -226,4 +230,5 @@ export default {
|
||||
setTmpSVGG,
|
||||
getChartThemeConfig,
|
||||
getChartConfig,
|
||||
getXYChartData,
|
||||
};
|
||||
|
||||
@@ -14,6 +14,7 @@ export const draw = (txt: string, id: string, _version: string, diagObj: Diagram
|
||||
const db = diagObj.db as typeof XYChartDB;
|
||||
const themeConfig = db.getChartThemeConfig();
|
||||
const chartConfig = db.getChartConfig();
|
||||
const labelData = db.getXYChartData().plots[0].data.map((data) => data[1]);
|
||||
function getDominantBaseLine(horizontalPos: TextVerticalPos) {
|
||||
return horizontalPos === 'top' ? 'text-before-edge' : 'middle';
|
||||
}
|
||||
@@ -49,6 +50,16 @@ export const draw = (txt: string, id: string, _version: string, diagObj: Diagram
|
||||
|
||||
const groups: Record<string, any> = {};
|
||||
|
||||
interface BarItem {
|
||||
data: {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
label: string;
|
||||
}
|
||||
|
||||
function getGroup(gList: string[]) {
|
||||
let elem = group;
|
||||
let prefix = '';
|
||||
@@ -87,6 +98,113 @@ export const draw = (txt: string, id: string, _version: string, diagObj: Diagram
|
||||
.attr('fill', (data) => data.fill)
|
||||
.attr('stroke', (data) => data.strokeFill)
|
||||
.attr('stroke-width', (data) => data.strokeWidth);
|
||||
|
||||
if (chartConfig.showDataLabel) {
|
||||
if (chartConfig.chartOrientation === 'horizontal') {
|
||||
// Factor to approximate each character's width.
|
||||
const charWidthFactor = 0.7;
|
||||
|
||||
// Filter out bars that have zero width or height.
|
||||
const validItems = shape.data
|
||||
.map((d, i) => ({ data: d, label: labelData[i].toString() }))
|
||||
.filter((item) => item.data.width > 0 && item.data.height > 0);
|
||||
|
||||
// Helper function to check if the text fits horizontally with a 10px right margin.
|
||||
function fitsHorizontally(item: BarItem, fontSize: number): boolean {
|
||||
const { data, label } = item;
|
||||
// Approximate the text width.
|
||||
const textWidth: number = fontSize * label.length * charWidthFactor;
|
||||
// The available width is the bar's width minus a 10px right margin.
|
||||
return textWidth <= data.width - 10;
|
||||
}
|
||||
|
||||
// For each valid bar, start with an initial candidate font size (70% of the bar's height),
|
||||
// then reduce it until the text fits horizontally.
|
||||
const candidateFontSizes = validItems.map((item) => {
|
||||
const { data } = item;
|
||||
let fontSize = data.height * 0.7;
|
||||
// Decrease fontSize until the text fits horizontally.
|
||||
while (!fitsHorizontally(item, fontSize) && fontSize > 0) {
|
||||
fontSize -= 1;
|
||||
}
|
||||
return fontSize;
|
||||
});
|
||||
|
||||
// Choose the smallest candidate font size across all valid bars for uniformity.
|
||||
const uniformFontSize = Math.floor(Math.min(...candidateFontSizes));
|
||||
|
||||
shapeGroup
|
||||
.selectAll('text')
|
||||
.data(validItems)
|
||||
.enter()
|
||||
.append('text')
|
||||
.attr('x', (item) => item.data.x + item.data.width - 10)
|
||||
.attr('y', (item) => item.data.y + item.data.height / 2)
|
||||
.attr('text-anchor', 'end')
|
||||
.attr('dominant-baseline', 'middle')
|
||||
.attr('fill', 'black')
|
||||
.attr('font-size', `${uniformFontSize}px`)
|
||||
.text((item) => item.label);
|
||||
} else {
|
||||
const yOffset = 10;
|
||||
|
||||
// filter out bars that have zero width or height.
|
||||
const validItems = shape.data
|
||||
.map((d, i) => ({ data: d, label: labelData[i].toString() }))
|
||||
.filter((item) => item.data.width > 0 && item.data.height > 0);
|
||||
|
||||
// Helper function that checks if the text with a given fontSize fits within the bar boundaries.
|
||||
function fitsInBar(item: BarItem, fontSize: number, yOffset: number): boolean {
|
||||
const { data, label } = item;
|
||||
const charWidthFactor = 0.7;
|
||||
const textWidth = fontSize * label.length * charWidthFactor;
|
||||
|
||||
// Compute horizontal boundaries using the center.
|
||||
const centerX = data.x + data.width / 2;
|
||||
const leftEdge = centerX - textWidth / 2;
|
||||
const rightEdge = centerX + textWidth / 2;
|
||||
|
||||
// Check that text doesn't overflow horizontally.
|
||||
const horizontalFits = leftEdge >= data.x && rightEdge <= data.x + data.width;
|
||||
|
||||
// For vertical placement, we use 'dominant-baseline: hanging' so that y marks the top of the text.
|
||||
// Thus, the bottom edge is y + yOffset + fontSize.
|
||||
const verticalFits = data.y + yOffset + fontSize <= data.y + data.height;
|
||||
|
||||
return horizontalFits && verticalFits;
|
||||
}
|
||||
|
||||
// For each valid item, start with a candidate font size based on the width,
|
||||
// then reduce it until the text fits within both the horizontal and vertical boundaries.
|
||||
const candidateFontSizes = validItems.map((item) => {
|
||||
const { data, label } = item;
|
||||
let fontSize = data.width / (label.length * 0.7);
|
||||
|
||||
// Decrease the font size until the text fits or fontSize reaches 0.
|
||||
while (!fitsInBar(item, fontSize, yOffset) && fontSize > 0) {
|
||||
fontSize -= 1;
|
||||
}
|
||||
return fontSize;
|
||||
});
|
||||
|
||||
// Choose the smallest candidate across all valid bars for uniformity.
|
||||
const uniformFontSize = Math.floor(Math.min(...candidateFontSizes));
|
||||
|
||||
// Render text only for valid items.
|
||||
shapeGroup
|
||||
.selectAll('text')
|
||||
.data(validItems)
|
||||
.enter()
|
||||
.append('text')
|
||||
.attr('x', (item) => item.data.x + item.data.width / 2)
|
||||
.attr('y', (item) => item.data.y + yOffset)
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('dominant-baseline', 'hanging')
|
||||
.attr('fill', 'black')
|
||||
.attr('font-size', `${uniformFontSize}px`)
|
||||
.text((item) => item.label);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'text':
|
||||
shapeGroup
|
||||
|
||||
@@ -75,7 +75,7 @@ When deployed within code, init is called before the graph/diagram description.
|
||||
|
||||
**for example**:
|
||||
|
||||
```mermaid
|
||||
```mermaid-example
|
||||
%%{init: {"theme": "default", "logLevel": 1 }}%%
|
||||
graph LR
|
||||
a-->b
|
||||
|
||||
@@ -88,7 +88,7 @@ Here the directive declaration will set the `logLevel` to `debug` and the `theme
|
||||
|
||||
Note: You can use 'init' or 'initialize' as both are acceptable as init directives. Also note that `%%init%%` and `%%initialize%%` directives will be grouped together after they are parsed.
|
||||
|
||||
```mermaid
|
||||
```mermaid-example
|
||||
%%{init: { 'logLevel': 'debug', 'theme': 'forest' } }%%
|
||||
%%{initialize: { 'logLevel': 'fatal', "theme":'dark', 'startOnLoad': true } }%%
|
||||
...
|
||||
|
||||
@@ -10,7 +10,7 @@ Note that at the moment, the only supported diagrams are below:
|
||||
|
||||
### Flowcharts
|
||||
|
||||
```mermaid
|
||||
```mermaid-example
|
||||
graph LR
|
||||
A["$$x^2$$"] -->|"$$\sqrt{x+3}$$"| B("$$\frac{1}{2}$$")
|
||||
A -->|"$$\overbrace{a+b+c}^{\text{note}}$$"| C("$$\pi r^2$$")
|
||||
@@ -20,7 +20,7 @@ Note that at the moment, the only supported diagrams are below:
|
||||
|
||||
### Sequence
|
||||
|
||||
```mermaid
|
||||
```mermaid-example
|
||||
sequenceDiagram
|
||||
autonumber
|
||||
participant 1 as $$\alpha$$
|
||||
|
||||
@@ -41,12 +41,6 @@ Example of `init` directive setting the `theme` to `forest`:
|
||||
a --> b
|
||||
```
|
||||
|
||||
```mermaid
|
||||
%%{init: {'theme':'forest'}}%%
|
||||
graph TD
|
||||
a --> b
|
||||
```
|
||||
|
||||
> **Reminder**: the only theme that can be customized is the `base` theme. The following section covers how to use `themeVariables` for customizations.
|
||||
|
||||
## Customizing Themes with `themeVariables`
|
||||
@@ -91,36 +85,6 @@ Example of modifying `themeVariables` using the `init` directive:
|
||||
end
|
||||
```
|
||||
|
||||
```mermaid
|
||||
%%{
|
||||
init: {
|
||||
'theme': 'base',
|
||||
'themeVariables': {
|
||||
'primaryColor': '#BB2528',
|
||||
'primaryTextColor': '#fff',
|
||||
'primaryBorderColor': '#7C0000',
|
||||
'lineColor': '#F8B229',
|
||||
'secondaryColor': '#006100',
|
||||
'tertiaryColor': '#fff'
|
||||
}
|
||||
}
|
||||
}%%
|
||||
graph TD
|
||||
A[Christmas] -->|Get money| B(Go shopping)
|
||||
B --> C{Let me think}
|
||||
B --> G[/Another/]
|
||||
C ==>|One| D[Laptop]
|
||||
C -->|Two| E[iPhone]
|
||||
C -->|Three| F[fa:fa-car Car]
|
||||
subgraph section
|
||||
C
|
||||
D
|
||||
E
|
||||
F
|
||||
G
|
||||
end
|
||||
```
|
||||
|
||||
## Color and Color Calculation
|
||||
|
||||
To ensure diagram readability, the default value of certain variables is calculated or derived from other variables. For example, `primaryBorderColor` is derived from the `primaryColor` variable. So if the `primaryColor` variable is customized, Mermaid will adjust `primaryBorderColor` automatically. Adjustments can mean a color inversion, a hue change, a darkening/lightening by 10%, etc.
|
||||
|
||||
@@ -35,6 +35,7 @@ To add an integration to this list, see the [Integrations - create page](./integ
|
||||
- [Mermaid Charts & Diagrams for Jira](https://marketplace.atlassian.com/apps/1224537/)
|
||||
- [Mermaid for Jira Cloud - Draw UML diagrams easily](https://marketplace.atlassian.com/apps/1223053/mermaid-for-jira-cloud-draw-uml-diagrams-easily?hosting=cloud&tab=overview)
|
||||
- [CloudScript.io Mermaid Addon](https://marketplace.atlassian.com/apps/1219878/cloudscript-io-mermaid-addon?hosting=cloud&tab=overview)
|
||||
- [Mermaid plus for Confluence](https://marketplace.atlassian.com/apps/1236814/mermaid-plus-for-confluence?hosting=cloud&tab=overview)
|
||||
- [Azure Devops](https://learn.microsoft.com/en-us/azure/devops/project/wiki/markdown-guidance?view=azure-devops#add-mermaid-diagrams-to-a-wiki-page) ✅
|
||||
- [Deepdwn](https://billiam.itch.io/deepdwn) ✅
|
||||
- [Doctave](https://www.doctave.com/) ✅
|
||||
@@ -262,7 +263,5 @@ Communication tools and platforms
|
||||
- [reveal.js-mermaid-plugin](https://github.com/ludwick/reveal.js-mermaid-plugin)
|
||||
- [Reveal CK](https://github.com/jedcn/reveal-ck)
|
||||
- [reveal-ck-mermaid-plugin](https://github.com/tmtm/reveal-ck-mermaid-plugin)
|
||||
- [mermaid-isomorphic](https://github.com/remcohaszing/mermaid-isomorphic)
|
||||
- [mermaid-server: Generate diagrams using a HTTP request](https://github.com/TomWright/mermaid-server)
|
||||
|
||||
<!--- cspell:ignore Blazorade HueHive --->
|
||||
|
||||
@@ -41,7 +41,7 @@ In the `Code` panel, write or edit Mermaid code, and instantly `Preview` the ren
|
||||
|
||||
Here is an example of Mermaid code and its rendered result:
|
||||
|
||||
```mermaid
|
||||
```mermaid-example
|
||||
graph TD
|
||||
A[Enter Chart Definition] --> B(Preview)
|
||||
B --> C{decide}
|
||||
|
||||
@@ -83,7 +83,7 @@ Mermaid offers a variety of styles or “looks” for your diagrams, allowing yo
|
||||
|
||||
You can select a look by adding the look parameter in the metadata section of your Mermaid diagram code. Here’s an example:
|
||||
|
||||
```mermaid
|
||||
```mermaid-example
|
||||
---
|
||||
config:
|
||||
look: handDrawn
|
||||
@@ -108,7 +108,7 @@ In addition to customizing the look of your diagrams, Mermaid Chart now allows y
|
||||
|
||||
You can specify the layout algorithm directly in the metadata section of your Mermaid diagram code. Here’s an example:
|
||||
|
||||
```mermaid
|
||||
```mermaid-example
|
||||
---
|
||||
config:
|
||||
layout: elk
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@mdi/font": "^7.4.47",
|
||||
"@vueuse/core": "^12.7.0",
|
||||
"@vueuse/core": "^13.1.0",
|
||||
"font-awesome": "^4.7.0",
|
||||
"jiti": "^2.4.2",
|
||||
"mermaid": "workspace:^",
|
||||
@@ -26,7 +26,7 @@
|
||||
"devDependencies": {
|
||||
"@iconify-json/carbon": "^1.1.37",
|
||||
"@unocss/reset": "^66.0.0",
|
||||
"@vite-pwa/vitepress": "^0.5.3",
|
||||
"@vite-pwa/vitepress": "^1.0.0",
|
||||
"@vitejs/plugin-vue": "^5.0.5",
|
||||
"fast-glob": "^3.3.3",
|
||||
"https-localhost": "^4.7.1",
|
||||
@@ -34,7 +34,7 @@
|
||||
"unocss": "^66.0.0",
|
||||
"unplugin-vue-components": "^28.4.0",
|
||||
"vite": "^6.1.1",
|
||||
"vite-plugin-pwa": "^0.21.1",
|
||||
"vite-plugin-pwa": "^1.0.0",
|
||||
"vitepress": "1.6.3",
|
||||
"workbox-window": "^7.3.0"
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ outline: 'deep' # shows all h3 headings in outline in Vitepress
|
||||
|
||||
## Introduction to Block Diagrams
|
||||
|
||||
```mermaid
|
||||
```mermaid-example
|
||||
block-beta
|
||||
columns 1
|
||||
db(("DB"))
|
||||
|
||||
@@ -248,7 +248,7 @@ classE o-- classF : aggregation
|
||||
|
||||
Relations can logically represent an N:M association:
|
||||
|
||||
```mermaid
|
||||
```mermaid-example
|
||||
classDiagram
|
||||
Animal <|--|> Zebra
|
||||
```
|
||||
|
||||
@@ -141,7 +141,7 @@ sequenceDiagram
|
||||
|
||||
## A commit flow diagram.
|
||||
|
||||
```mermaid
|
||||
```mermaid-example
|
||||
gitGraph:
|
||||
commit "Ashish"
|
||||
branch newbranch
|
||||
|
||||
@@ -721,7 +721,7 @@ To give an edge an ID, prepend the edge syntax with the ID followed by an `@` ch
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A e1@–> B
|
||||
A e1@--> B
|
||||
```
|
||||
|
||||
In this example, `e1` is the ID of the edge connecting `A` to `B`. You can then use this ID in later definitions or style statements, just like with nodes.
|
||||
@@ -746,7 +746,7 @@ In the initial version, two animation speeds are supported: `fast` and `slow`. S
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A e1@–> B
|
||||
A e1@--> B
|
||||
e1@{ animation: fast }
|
||||
```
|
||||
|
||||
@@ -758,7 +758,7 @@ You can also animate edges by assigning a class to them and then defining animat
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A e1@–> B
|
||||
A e1@--> B
|
||||
classDef animate stroke-dasharray: 9,5,stroke-dashoffset: 900,animation: dash 25s linear infinite;
|
||||
class e1 animate
|
||||
```
|
||||
|
||||
@@ -259,7 +259,7 @@ gantt
|
||||
|
||||
The compact mode allows you to display multiple tasks in the same row. Compact mode can be enabled for a gantt chart by setting the display mode of the graph via preceding YAML settings.
|
||||
|
||||
```mermaid
|
||||
```mermaid-example
|
||||
---
|
||||
displayMode: compact
|
||||
---
|
||||
|
||||
@@ -290,7 +290,13 @@ Sometimes you may want to hide the branch names and lines from the diagram. You
|
||||
Usage example:
|
||||
|
||||
```mermaid-example
|
||||
%%{init: { 'logLevel': 'debug', 'theme': 'base', 'gitGraph': {'showBranches': false}} }%%
|
||||
---
|
||||
config:
|
||||
logLevel: 'debug'
|
||||
theme: 'base'
|
||||
gitGraph:
|
||||
showBranches: false
|
||||
---
|
||||
gitGraph
|
||||
commit
|
||||
branch hotfix
|
||||
@@ -346,7 +352,13 @@ You can change the layout of the commit labels by using the `rotateCommitLabel`
|
||||
Usage example: Rotated commit labels
|
||||
|
||||
```mermaid-example
|
||||
%%{init: { 'logLevel': 'debug', 'theme': 'base', 'gitGraph': {'rotateCommitLabel': true}} }%%
|
||||
---
|
||||
config:
|
||||
logLevel: 'debug'
|
||||
theme: 'base'
|
||||
gitGraph:
|
||||
rotateCommitLabel: true
|
||||
---
|
||||
gitGraph
|
||||
commit id: "feat(api): ..."
|
||||
commit id: "a"
|
||||
@@ -367,7 +379,13 @@ gitGraph
|
||||
Usage example: Horizontal commit labels
|
||||
|
||||
```mermaid-example
|
||||
%%{init: { 'logLevel': 'debug', 'theme': 'base', 'gitGraph': {'rotateCommitLabel': false}} }%%
|
||||
---
|
||||
config:
|
||||
logLevel: 'debug'
|
||||
theme: 'base'
|
||||
gitGraph:
|
||||
rotateCommitLabel: false
|
||||
---
|
||||
gitGraph
|
||||
commit id: "feat(api): ..."
|
||||
commit id: "a"
|
||||
@@ -392,7 +410,14 @@ Sometimes you may want to hide the commit labels from the diagram. You can do th
|
||||
Usage example:
|
||||
|
||||
```mermaid-example
|
||||
%%{init: { 'logLevel': 'debug', 'theme': 'base', 'gitGraph': {'showBranches': false,'showCommitLabel': false}} }%%
|
||||
---
|
||||
config:
|
||||
logLevel: 'debug'
|
||||
theme: 'base'
|
||||
gitGraph:
|
||||
showBranches: false
|
||||
showCommitLabel: false
|
||||
---
|
||||
gitGraph
|
||||
commit
|
||||
branch hotfix
|
||||
@@ -444,7 +469,15 @@ Sometimes you may want to customize the name of the main/default branch. You can
|
||||
Usage example:
|
||||
|
||||
```mermaid-example
|
||||
%%{init: { 'logLevel': 'debug', 'theme': 'base', 'gitGraph': {'showBranches': true, 'showCommitLabel':true,'mainBranchName': 'MetroLine1'}} }%%
|
||||
---
|
||||
config:
|
||||
logLevel: 'debug'
|
||||
theme: 'base'
|
||||
gitGraph:
|
||||
showBranches: true
|
||||
showCommitLabel: true
|
||||
mainBranchName: 'MetroLine1'
|
||||
---
|
||||
gitGraph
|
||||
commit id:"NewYork"
|
||||
commit id:"Dallas"
|
||||
@@ -486,7 +519,14 @@ To fully control the order of all the branches, you must define `order` for all
|
||||
Usage example:
|
||||
|
||||
```mermaid-example
|
||||
%%{init: { 'logLevel': 'debug', 'theme': 'base', 'gitGraph': {'showBranches': true, 'showCommitLabel':true}} }%%
|
||||
---
|
||||
config:
|
||||
logLevel: 'debug'
|
||||
theme: 'base'
|
||||
gitGraph:
|
||||
showBranches: true
|
||||
showCommitLabel: true
|
||||
---
|
||||
gitGraph
|
||||
commit
|
||||
branch test1 order: 3
|
||||
@@ -500,7 +540,15 @@ Look at the diagram, all the branches are following the order defined.
|
||||
Usage example:
|
||||
|
||||
```mermaid-example
|
||||
%%{init: { 'logLevel': 'debug', 'theme': 'base', 'gitGraph': {'showBranches': true, 'showCommitLabel':true,'mainBranchOrder': 2}} }%%
|
||||
---
|
||||
config:
|
||||
logLevel: 'debug'
|
||||
theme: 'base'
|
||||
gitGraph:
|
||||
showBranches: true
|
||||
showCommitLabel: true
|
||||
mainBranchOrder: 2
|
||||
---
|
||||
gitGraph
|
||||
commit
|
||||
branch test1 order: 3
|
||||
@@ -652,7 +700,11 @@ Let's put them to use, and see how our sample diagram looks in different themes:
|
||||
### Base Theme
|
||||
|
||||
```mermaid-example
|
||||
%%{init: { 'logLevel': 'debug', 'theme': 'base' } }%%
|
||||
---
|
||||
config:
|
||||
logLevel: 'debug'
|
||||
theme: 'base'
|
||||
---
|
||||
gitGraph
|
||||
commit
|
||||
branch hotfix
|
||||
@@ -700,7 +752,11 @@ Let's put them to use, and see how our sample diagram looks in different themes:
|
||||
### Forest Theme
|
||||
|
||||
```mermaid-example
|
||||
%%{init: { 'logLevel': 'debug', 'theme': 'forest' } }%%
|
||||
---
|
||||
config:
|
||||
logLevel: 'debug'
|
||||
theme: 'forest'
|
||||
---
|
||||
gitGraph
|
||||
commit
|
||||
branch hotfix
|
||||
@@ -748,7 +804,11 @@ Let's put them to use, and see how our sample diagram looks in different themes:
|
||||
### Default Theme
|
||||
|
||||
```mermaid-example
|
||||
%%{init: { 'logLevel': 'debug', 'theme': 'default' } }%%
|
||||
---
|
||||
config:
|
||||
logLevel: 'debug'
|
||||
theme: 'default'
|
||||
---
|
||||
gitGraph
|
||||
commit type:HIGHLIGHT
|
||||
branch hotfix
|
||||
@@ -796,7 +856,11 @@ Let's put them to use, and see how our sample diagram looks in different themes:
|
||||
### Dark Theme
|
||||
|
||||
```mermaid-example
|
||||
%%{init: { 'logLevel': 'debug', 'theme': 'dark' } }%%
|
||||
---
|
||||
config:
|
||||
logLevel: 'debug'
|
||||
theme: 'dark'
|
||||
---
|
||||
gitGraph
|
||||
commit
|
||||
branch hotfix
|
||||
@@ -844,7 +908,11 @@ Let's put them to use, and see how our sample diagram looks in different themes:
|
||||
### Neutral Theme
|
||||
|
||||
```mermaid-example
|
||||
%%{init: { 'logLevel': 'debug', 'theme': 'neutral' } }%%
|
||||
---
|
||||
config:
|
||||
logLevel: 'debug'
|
||||
theme: 'neutral'
|
||||
---
|
||||
gitGraph
|
||||
commit
|
||||
branch hotfix
|
||||
@@ -898,7 +966,11 @@ For understanding let us take a sample diagram with theme `default`, the default
|
||||
See how the default theme is used to set the colors for the branches:
|
||||
|
||||
```mermaid-example
|
||||
%%{init: { 'logLevel': 'debug', 'theme': 'default' } }%%
|
||||
---
|
||||
config:
|
||||
logLevel: 'debug'
|
||||
theme: 'default'
|
||||
---
|
||||
gitGraph
|
||||
commit
|
||||
branch develop
|
||||
@@ -929,16 +1001,20 @@ Example:
|
||||
Now let's override the default values for the `git0` to `git3` variables:
|
||||
|
||||
```mermaid-example
|
||||
%%{init: { 'logLevel': 'debug', 'theme': 'default' , 'themeVariables': {
|
||||
'git0': '#ff0000',
|
||||
'git1': '#00ff00',
|
||||
'git2': '#0000ff',
|
||||
'git3': '#ff00ff',
|
||||
'git4': '#00ffff',
|
||||
'git5': '#ffff00',
|
||||
'git6': '#ff00ff',
|
||||
'git7': '#00ffff'
|
||||
} } }%%
|
||||
---
|
||||
config:
|
||||
logLevel: 'debug'
|
||||
theme: 'default'
|
||||
themeVariables:
|
||||
'git0': '#ff0000'
|
||||
'git1': '#00ff00'
|
||||
'git2': '#0000ff'
|
||||
'git3': '#ff00ff'
|
||||
'git4': '#00ffff'
|
||||
'git5': '#ffff00'
|
||||
'git6': '#ff00ff'
|
||||
'git7': '#00ffff'
|
||||
---
|
||||
gitGraph
|
||||
commit
|
||||
branch develop
|
||||
@@ -965,18 +1041,22 @@ Lets see how the default theme is used to set the colors for the branch labels:
|
||||
Now let's override the default values for the `gitBranchLabel0` to `gitBranchLabel2` variables:
|
||||
|
||||
```mermaid-example
|
||||
%%{init: { 'logLevel': 'debug', 'theme': 'default' , 'themeVariables': {
|
||||
'gitBranchLabel0': '#ffffff',
|
||||
'gitBranchLabel1': '#ffffff',
|
||||
'gitBranchLabel2': '#ffffff',
|
||||
'gitBranchLabel3': '#ffffff',
|
||||
'gitBranchLabel4': '#ffffff',
|
||||
'gitBranchLabel5': '#ffffff',
|
||||
'gitBranchLabel6': '#ffffff',
|
||||
'gitBranchLabel7': '#ffffff',
|
||||
'gitBranchLabel8': '#ffffff',
|
||||
'gitBranchLabel9': '#ffffff'
|
||||
} } }%%
|
||||
---
|
||||
config:
|
||||
logLevel: 'debug'
|
||||
theme: 'default'
|
||||
themeVariables:
|
||||
'gitBranchLabel0': '#ffffff'
|
||||
'gitBranchLabel1': '#ffffff'
|
||||
'gitBranchLabel2': '#ffffff'
|
||||
'gitBranchLabel3': '#ffffff'
|
||||
'gitBranchLabel4': '#ffffff'
|
||||
'gitBranchLabel5': '#ffffff'
|
||||
'gitBranchLabel6': '#ffffff'
|
||||
'gitBranchLabel7': '#ffffff'
|
||||
'gitBranchLabel8': '#ffffff'
|
||||
'gitBranchLabel9': '#ffffff'
|
||||
---
|
||||
gitGraph
|
||||
checkout main
|
||||
branch branch1
|
||||
@@ -1002,10 +1082,14 @@ Example:
|
||||
Now let's override the default values for the `commitLabelColor` to `commitLabelBackground` variables:
|
||||
|
||||
```mermaid-example
|
||||
%%{init: { 'logLevel': 'debug', 'theme': 'default' , 'themeVariables': {
|
||||
'commitLabelColor': '#ff0000',
|
||||
'commitLabelBackground': '#00ff00'
|
||||
} } }%%
|
||||
---
|
||||
config:
|
||||
logLevel: 'debug'
|
||||
theme: 'default'
|
||||
themeVariables:
|
||||
commitLabelColor: '#ff0000'
|
||||
commitLabelBackground: '#00ff00'
|
||||
---
|
||||
gitGraph
|
||||
commit
|
||||
branch develop
|
||||
@@ -1031,11 +1115,15 @@ Example:
|
||||
Now let's override the default values for the `commitLabelFontSize` variable:
|
||||
|
||||
```mermaid-example
|
||||
%%{init: { 'logLevel': 'debug', 'theme': 'default' , 'themeVariables': {
|
||||
'commitLabelColor': '#ff0000',
|
||||
'commitLabelBackground': '#00ff00',
|
||||
'commitLabelFontSize': '16px'
|
||||
} } }%%
|
||||
---
|
||||
config:
|
||||
logLevel: 'debug'
|
||||
theme: 'default'
|
||||
themeVariables:
|
||||
commitLabelColor: '#ff0000'
|
||||
commitLabelBackground: '#00ff00'
|
||||
commitLabelFontSize: '16px'
|
||||
---
|
||||
gitGraph
|
||||
commit
|
||||
branch develop
|
||||
@@ -1061,11 +1149,15 @@ Example:
|
||||
Now let's override the default values for the `tagLabelFontSize` variable:
|
||||
|
||||
```mermaid-example
|
||||
%%{init: { 'logLevel': 'debug', 'theme': 'default' , 'themeVariables': {
|
||||
'commitLabelColor': '#ff0000',
|
||||
'commitLabelBackground': '#00ff00',
|
||||
'tagLabelFontSize': '16px'
|
||||
} } }%%
|
||||
---
|
||||
config:
|
||||
logLevel: 'debug'
|
||||
theme: 'default'
|
||||
themeVariables:
|
||||
commitLabelColor: '#ff0000'
|
||||
commitLabelBackground: '#00ff00'
|
||||
tagLabelFontSize: '16px'
|
||||
---
|
||||
gitGraph
|
||||
commit
|
||||
branch develop
|
||||
@@ -1090,11 +1182,15 @@ Example:
|
||||
Now let's override the default values for the `tagLabelColor`, `tagLabelBackground` and to `tagLabelBorder` variables:
|
||||
|
||||
```mermaid-example
|
||||
%%{init: { 'logLevel': 'debug', 'theme': 'default' , 'themeVariables': {
|
||||
'tagLabelColor': '#ff0000',
|
||||
'tagLabelBackground': '#00ff00',
|
||||
'tagLabelBorder': '#0000ff'
|
||||
} } }%%
|
||||
---
|
||||
config:
|
||||
logLevel: 'debug'
|
||||
theme: 'default'
|
||||
themeVariables:
|
||||
tagLabelColor: '#ff0000'
|
||||
tagLabelBackground: '#00ff00'
|
||||
tagLabelBorder: '#0000ff'
|
||||
---
|
||||
gitGraph
|
||||
commit
|
||||
branch develop
|
||||
@@ -1121,9 +1217,13 @@ Example:
|
||||
Now let's override the default values for the `git0` to `git3` variables:
|
||||
|
||||
```mermaid-example
|
||||
%%{init: { 'logLevel': 'debug', 'theme': 'default' , 'themeVariables': {
|
||||
'gitInv0': '#ff0000'
|
||||
} } }%%
|
||||
---
|
||||
config:
|
||||
logLevel: 'debug'
|
||||
theme: 'default'
|
||||
themeVariables:
|
||||
'gitInv0': '#ff0000'
|
||||
---
|
||||
gitGraph
|
||||
commit
|
||||
branch develop
|
||||
|
||||
@@ -442,7 +442,7 @@ sequenceDiagram
|
||||
|
||||
Comments can be entered within a sequence diagram, which will be ignored by the parser. Comments need to be on their own line, and must be prefaced with `%%` (double percent signs). Any text after the start of the comment to the next newline will be treated as a comment, including any diagram syntax
|
||||
|
||||
```mermaid
|
||||
```mermaid-example
|
||||
sequenceDiagram
|
||||
Alice->>John: Hello John, how are you?
|
||||
%% this is a comment
|
||||
|
||||
@@ -95,17 +95,18 @@ xychart-beta
|
||||
|
||||
## Chart Configurations
|
||||
|
||||
| Parameter | Description | Default value |
|
||||
| ------------------------ | ---------------------------------------------- | :-----------: |
|
||||
| width | Width of the chart | 700 |
|
||||
| height | Height of the chart | 500 |
|
||||
| titlePadding | Top and Bottom padding of the title | 10 |
|
||||
| titleFontSize | Title font size | 20 |
|
||||
| showTitle | Title to be shown or not | true |
|
||||
| xAxis | xAxis configuration | AxisConfig |
|
||||
| yAxis | yAxis configuration | AxisConfig |
|
||||
| chartOrientation | 'vertical' or 'horizontal' | 'vertical' |
|
||||
| plotReservedSpacePercent | Minimum space plots will take inside the chart | 50 |
|
||||
| Parameter | Description | Default value |
|
||||
| ------------------------ | ------------------------------------------------------------- | :-----------: |
|
||||
| width | Width of the chart | 700 |
|
||||
| height | Height of the chart | 500 |
|
||||
| titlePadding | Top and Bottom padding of the title | 10 |
|
||||
| titleFontSize | Title font size | 20 |
|
||||
| showTitle | Title to be shown or not | true |
|
||||
| xAxis | xAxis configuration | AxisConfig |
|
||||
| yAxis | yAxis configuration | AxisConfig |
|
||||
| chartOrientation | 'vertical' or 'horizontal' | 'vertical' |
|
||||
| plotReservedSpacePercent | Minimum space plots will take inside the chart | 50 |
|
||||
| showDataLabel | Should show the value corresponding to the bar within the bar | false |
|
||||
|
||||
### AxisConfig
|
||||
|
||||
@@ -152,6 +153,7 @@ config:
|
||||
xyChart:
|
||||
width: 900
|
||||
height: 600
|
||||
showDataLabel: true
|
||||
themeVariables:
|
||||
xyChart:
|
||||
titleColor: "#ff0000"
|
||||
|
||||
@@ -31,6 +31,7 @@ vi.mock('./diagrams/xychart/xychartRenderer.js');
|
||||
vi.mock('./diagrams/requirement/requirementRenderer.js');
|
||||
vi.mock('./diagrams/sequence/sequenceRenderer.js');
|
||||
vi.mock('./diagrams/radar/renderer.js');
|
||||
vi.mock('./diagrams/architecture/architectureRenderer.js');
|
||||
|
||||
// -------------------------------------
|
||||
|
||||
@@ -799,6 +800,7 @@ graph TD;A--x|text including URL space|B;`)
|
||||
{ textDiagramType: 'sequenceDiagram', expectedType: 'sequence' },
|
||||
{ textDiagramType: 'stateDiagram-v2', expectedType: 'stateDiagram' },
|
||||
{ textDiagramType: 'radar-beta', expectedType: 'radar' },
|
||||
{ textDiagramType: 'architecture-beta', expectedType: 'architecture' },
|
||||
];
|
||||
|
||||
describe('accessibility', () => {
|
||||
|
||||
@@ -562,7 +562,7 @@ export const insertEdge = function (elem, edge, clusterDb, diagramType, startNod
|
||||
}
|
||||
let svgPath;
|
||||
let linePath = lineFunction(lineData);
|
||||
const edgeStyles = Array.isArray(edge.style) ? edge.style : [edge.style];
|
||||
const edgeStyles = Array.isArray(edge.style) ? edge.style : edge.style ? [edge.style] : [];
|
||||
let strokeColor = edgeStyles.find((style) => style?.startsWith('stroke:'));
|
||||
|
||||
if (edge.look === 'handDrawn') {
|
||||
|
||||
@@ -1228,6 +1228,10 @@ $defs: # JSON Schema definition (maybe we should move these to a separate file)
|
||||
type: number
|
||||
default: 10
|
||||
minimum: 0
|
||||
showDataLabel:
|
||||
description: Should show the value corresponding to the bar within the bar
|
||||
type: boolean
|
||||
default: false
|
||||
showTitle:
|
||||
description: Should show the chart title
|
||||
type: boolean
|
||||
@@ -1484,6 +1488,9 @@ $defs: # JSON Schema definition (maybe we should move these to a separate file)
|
||||
- bottomMarginAdj
|
||||
- useMaxWidth
|
||||
- rightAngles
|
||||
- titleColor
|
||||
- titleFontFamily
|
||||
- titleFontSize
|
||||
properties:
|
||||
diagramMarginX:
|
||||
$ref: '#/$defs/C4DiagramConfig/properties/diagramMarginX'
|
||||
@@ -1496,6 +1503,10 @@ $defs: # JSON Schema definition (maybe we should move these to a separate file)
|
||||
type: integer
|
||||
default: 150
|
||||
minimum: 0
|
||||
maxLabelWidth:
|
||||
description: Maximum width of actor labels
|
||||
type: integer
|
||||
default: 360
|
||||
width:
|
||||
description: Width of actor boxes
|
||||
type: integer
|
||||
@@ -1584,6 +1595,18 @@ $defs: # JSON Schema definition (maybe we should move these to a separate file)
|
||||
items:
|
||||
type: string
|
||||
default: ['#fff']
|
||||
titleColor:
|
||||
description: Color of the title text in Journey Diagrams
|
||||
type: string
|
||||
default: ''
|
||||
titleFontFamily:
|
||||
description: Font family to be used for the title text in Journey Diagrams
|
||||
type: string
|
||||
default: '"trebuchet ms", verdana, arial, sans-serif'
|
||||
titleFontSize:
|
||||
description: Font size to be used for the title text in Journey Diagrams
|
||||
type: string
|
||||
default: '4ex'
|
||||
|
||||
TimelineDiagramConfig:
|
||||
# added by https://github.com/mermaid-js/mermaid/commit/0d5246fbc730bf15463d7183fe4400a1e2fc492c
|
||||
|
||||
2
packages/parser/src/language/architecture/arch.langium
Normal file
2
packages/parser/src/language/architecture/arch.langium
Normal file
@@ -0,0 +1,2 @@
|
||||
terminal ARCH_ICON: /\([\w-:]+\)/;
|
||||
terminal ARCH_TITLE: /\[[\w ]+\]/;
|
||||
@@ -1,14 +1,15 @@
|
||||
grammar Architecture
|
||||
import "../common/common";
|
||||
import "arch";
|
||||
|
||||
entry Architecture:
|
||||
NEWLINE*
|
||||
"architecture-beta"
|
||||
(
|
||||
NEWLINE* TitleAndAccessibilities
|
||||
| NEWLINE* Statement*
|
||||
| NEWLINE*
|
||||
)
|
||||
NEWLINE
|
||||
| TitleAndAccessibilities
|
||||
| Statement
|
||||
)*
|
||||
;
|
||||
|
||||
fragment Statement:
|
||||
@@ -31,25 +32,21 @@ fragment Arrow:
|
||||
;
|
||||
|
||||
Group:
|
||||
'group' id=ARCH_ID icon=ARCH_ICON? title=ARCH_TITLE? ('in' in=ARCH_ID)? EOL
|
||||
'group' id=ID icon=ARCH_ICON? title=ARCH_TITLE? ('in' in=ID)? EOL
|
||||
;
|
||||
|
||||
Service:
|
||||
'service' id=ARCH_ID (iconText=ARCH_TEXT_ICON | icon=ARCH_ICON)? title=ARCH_TITLE? ('in' in=ARCH_ID)? EOL
|
||||
'service' id=ID (iconText=STRING | icon=ARCH_ICON)? title=ARCH_TITLE? ('in' in=ID)? EOL
|
||||
;
|
||||
|
||||
Junction:
|
||||
'junction' id=ARCH_ID ('in' in=ARCH_ID)? EOL
|
||||
'junction' id=ID ('in' in=ID)? EOL
|
||||
;
|
||||
|
||||
Edge:
|
||||
lhsId=ARCH_ID lhsGroup?=ARROW_GROUP? Arrow rhsId=ARCH_ID rhsGroup?=ARROW_GROUP? EOL
|
||||
lhsId=ID lhsGroup?=ARROW_GROUP? Arrow rhsId=ID rhsGroup?=ARROW_GROUP? EOL
|
||||
;
|
||||
|
||||
terminal ARROW_DIRECTION: 'L' | 'R' | 'T' | 'B';
|
||||
terminal ARCH_ID: /[\w]+/;
|
||||
terminal ARCH_TEXT_ICON: /\("[^"]+"\)/;
|
||||
terminal ARCH_ICON: /\([\w-:]+\)/;
|
||||
terminal ARCH_TITLE: /\[[\w ]+\]/;
|
||||
terminal ARROW_GROUP: /\{group\}/;
|
||||
terminal ARROW_INTO: /<|>/;
|
||||
|
||||
@@ -1,22 +1,35 @@
|
||||
interface Common {
|
||||
accDescr?: string;
|
||||
accTitle?: string;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
fragment TitleAndAccessibilities:
|
||||
((accDescr=ACC_DESCR | accTitle=ACC_TITLE | title=TITLE) EOL)+
|
||||
;
|
||||
// Base terminals and fragments for common language constructs
|
||||
// Terminal Precedence: Lazy to Greedy
|
||||
// When imported, the terminals are considered after the terminals in the importing grammar
|
||||
// Note: Hence, to add a terminal greedier than the common terminals, import it separately after the common import
|
||||
|
||||
fragment EOL returns string:
|
||||
NEWLINE+ | EOF
|
||||
;
|
||||
|
||||
terminal NEWLINE: /\r?\n/;
|
||||
fragment TitleAndAccessibilities:
|
||||
((accDescr=ACC_DESCR | accTitle=ACC_TITLE | title=TITLE) EOL)+
|
||||
;
|
||||
|
||||
terminal BOOLEAN returns boolean: 'true' | 'false';
|
||||
|
||||
terminal ACC_DESCR: /[\t ]*accDescr(?:[\t ]*:([^\n\r]*?(?=%%)|[^\n\r]*)|\s*{([^}]*)})/;
|
||||
terminal ACC_TITLE: /[\t ]*accTitle[\t ]*:(?:[^\n\r]*?(?=%%)|[^\n\r]*)/;
|
||||
terminal TITLE: /[\t ]*title(?:[\t ][^\n\r]*?(?=%%)|[\t ][^\n\r]*|)/;
|
||||
|
||||
terminal FLOAT returns number: /[0-9]+\.[0-9]+(?!\.)/;
|
||||
terminal INT returns number: /0|[1-9][0-9]*(?!\.)/;
|
||||
terminal NUMBER returns number: FLOAT | INT;
|
||||
|
||||
terminal STRING returns string: /"([^"\\]|\\.)*"|'([^'\\]|\\.)*'/;
|
||||
|
||||
// Alphanumerics with underscores and dashes
|
||||
// Must start with an alphanumeric or an underscore
|
||||
// Cant end with a dash
|
||||
terminal ID returns string: /[\w]([-\w]*\w)?/;
|
||||
|
||||
terminal NEWLINE: /\r?\n/;
|
||||
|
||||
hidden terminal WHITESPACE: /[\t ]+/;
|
||||
hidden terminal YAML: /---[\t ]*\r?\n(?:[\S\s]*?\r?\n)?---(?:\r?\n|(?!\S))/;
|
||||
hidden terminal DIRECTIVE: /[\t ]*%%{[\S\s]*?}%%(?:\r?\n|(?!\S))/;
|
||||
|
||||
@@ -1,39 +1,15 @@
|
||||
grammar GitGraph
|
||||
|
||||
interface Common {
|
||||
accDescr?: string;
|
||||
accTitle?: string;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
fragment TitleAndAccessibilities:
|
||||
((accDescr=ACC_DESCR | accTitle=ACC_TITLE | title=TITLE) EOL)+
|
||||
;
|
||||
|
||||
fragment EOL returns string:
|
||||
NEWLINE+ | EOF
|
||||
;
|
||||
|
||||
terminal NEWLINE: /\r?\n/;
|
||||
terminal ACC_DESCR: /[\t ]*accDescr(?:[\t ]*:([^\n\r]*?(?=%%)|[^\n\r]*)|\s*{([^}]*)})/;
|
||||
terminal ACC_TITLE: /[\t ]*accTitle[\t ]*:(?:[^\n\r]*?(?=%%)|[^\n\r]*)/;
|
||||
terminal TITLE: /[\t ]*title(?:[\t ][^\n\r]*?(?=%%)|[\t ][^\n\r]*|)/;
|
||||
|
||||
hidden terminal WHITESPACE: /[\t ]+/;
|
||||
hidden terminal YAML: /---[\t ]*\r?\n(?:[\S\s]*?\r?\n)?---(?:\r?\n|(?!\S))/;
|
||||
hidden terminal DIRECTIVE: /[\t ]*%%{[\S\s]*?}%%(?:\r?\n|(?!\S))/;
|
||||
hidden terminal SINGLE_LINE_COMMENT: /[\t ]*%%[^\n\r]*/;
|
||||
import "../common/common";
|
||||
import "reference";
|
||||
|
||||
entry GitGraph:
|
||||
NEWLINE*
|
||||
('gitGraph' | 'gitGraph' ':' | 'gitGraph:' | ('gitGraph' Direction ':'))
|
||||
NEWLINE*
|
||||
(
|
||||
NEWLINE*
|
||||
(TitleAndAccessibilities |
|
||||
statements+=Statement |
|
||||
NEWLINE)*
|
||||
)
|
||||
NEWLINE
|
||||
| TitleAndAccessibilities
|
||||
| statements+=Statement
|
||||
)*
|
||||
;
|
||||
|
||||
Statement
|
||||
@@ -56,12 +32,12 @@ Commit:
|
||||
|'type:' type=('NORMAL' | 'REVERSE' | 'HIGHLIGHT')
|
||||
)* EOL;
|
||||
Branch:
|
||||
'branch' name=(ID|STRING)
|
||||
'branch' name=(REFERENCE|STRING)
|
||||
('order:' order=INT)?
|
||||
EOL;
|
||||
|
||||
Merge:
|
||||
'merge' branch=(ID|STRING)
|
||||
'merge' branch=(REFERENCE|STRING)
|
||||
(
|
||||
'id:' id=STRING
|
||||
|'tag:' tags+=STRING
|
||||
@@ -69,7 +45,7 @@ Merge:
|
||||
)* EOL;
|
||||
|
||||
Checkout:
|
||||
('checkout'|'switch') branch=(ID|STRING) EOL;
|
||||
('checkout'|'switch') branch=(REFERENCE|STRING) EOL;
|
||||
|
||||
CherryPicking:
|
||||
'cherry-pick'
|
||||
@@ -78,10 +54,3 @@ CherryPicking:
|
||||
|'tag:' tags+=STRING
|
||||
|'parent:' parent=STRING
|
||||
)* EOL;
|
||||
|
||||
|
||||
|
||||
terminal INT returns number: /[0-9]+(?=\s)/;
|
||||
terminal ID returns string: /\w([-\./\w]*[-\w])?/;
|
||||
terminal STRING: /"[^"]*"|'[^']*'/;
|
||||
|
||||
|
||||
4
packages/parser/src/language/gitGraph/reference.langium
Normal file
4
packages/parser/src/language/gitGraph/reference.langium
Normal file
@@ -0,0 +1,4 @@
|
||||
// Alphanumerics with underscores, dashes, slashes, and dots
|
||||
// Must start with an alphanumeric or an underscore
|
||||
// Cant end with a dash, slash, or dot
|
||||
terminal REFERENCE returns string: /\w([-\./\w]*[-\w])?/;
|
||||
@@ -12,7 +12,6 @@ export {
|
||||
Commit,
|
||||
Merge,
|
||||
Statement,
|
||||
isCommon,
|
||||
isInfo,
|
||||
isPacket,
|
||||
isPacketBlock,
|
||||
|
||||
@@ -5,15 +5,12 @@ entry Packet:
|
||||
NEWLINE*
|
||||
"packet-beta"
|
||||
(
|
||||
NEWLINE* TitleAndAccessibilities blocks+=PacketBlock*
|
||||
| NEWLINE+ blocks+=PacketBlock+
|
||||
| NEWLINE*
|
||||
)
|
||||
TitleAndAccessibilities
|
||||
| blocks+=PacketBlock
|
||||
| NEWLINE
|
||||
)*
|
||||
;
|
||||
|
||||
PacketBlock:
|
||||
start=INT('-' end=INT)? ':' label=STRING EOL
|
||||
;
|
||||
|
||||
terminal INT returns number: /0|[1-9][0-9]*/;
|
||||
terminal STRING: /"[^"]*"|'[^']*'/;
|
||||
;
|
||||
@@ -5,15 +5,12 @@ entry Pie:
|
||||
NEWLINE*
|
||||
"pie" showData?="showData"?
|
||||
(
|
||||
NEWLINE* TitleAndAccessibilities sections+=PieSection*
|
||||
| NEWLINE+ sections+=PieSection+
|
||||
| NEWLINE*
|
||||
)
|
||||
TitleAndAccessibilities
|
||||
| sections+=PieSection
|
||||
| NEWLINE
|
||||
)*
|
||||
;
|
||||
|
||||
PieSection:
|
||||
label=PIE_SECTION_LABEL ":" value=PIE_SECTION_VALUE EOL
|
||||
label=STRING ":" value=NUMBER EOL
|
||||
;
|
||||
|
||||
terminal PIE_SECTION_LABEL: /"[^"]+"/;
|
||||
terminal PIE_SECTION_VALUE returns number: /(0|[1-9][0-9]*)(\.[0-9]+)?/;
|
||||
|
||||
@@ -1,31 +1,5 @@
|
||||
grammar Radar
|
||||
// import "../common/common";
|
||||
// Note: The import statement breaks TitleAndAccessibilities probably because of terminal order definition
|
||||
// TODO: May need to change the common.langium to fix this
|
||||
|
||||
interface Common {
|
||||
accDescr?: string;
|
||||
accTitle?: string;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
fragment TitleAndAccessibilities:
|
||||
((accDescr=ACC_DESCR | accTitle=ACC_TITLE | title=TITLE) EOL)+
|
||||
;
|
||||
|
||||
fragment EOL returns string:
|
||||
NEWLINE+ | EOF
|
||||
;
|
||||
|
||||
terminal NEWLINE: /\r?\n/;
|
||||
terminal ACC_DESCR: /[\t ]*accDescr(?:[\t ]*:([^\n\r]*?(?=%%)|[^\n\r]*)|\s*{([^}]*)})/;
|
||||
terminal ACC_TITLE: /[\t ]*accTitle[\t ]*:(?:[^\n\r]*?(?=%%)|[^\n\r]*)/;
|
||||
terminal TITLE: /[\t ]*title(?:[\t ][^\n\r]*?(?=%%)|[\t ][^\n\r]*|)/;
|
||||
|
||||
hidden terminal WHITESPACE: /[\t ]+/;
|
||||
hidden terminal YAML: /---[\t ]*\r?\n(?:[\S\s]*?\r?\n)?---(?:\r?\n|(?!\S))/;
|
||||
hidden terminal DIRECTIVE: /[\t ]*%%{[\S\s]*?}%%(?:\r?\n|(?!\S))/;
|
||||
hidden terminal SINGLE_LINE_COMMENT: /[\t ]*%%[^\n\r]*/;
|
||||
import "../common/common";
|
||||
|
||||
entry Radar:
|
||||
NEWLINE*
|
||||
@@ -76,14 +50,6 @@ Option:
|
||||
| name='min' value=NUMBER
|
||||
| name='graticule' value=GRATICULE
|
||||
)
|
||||
;
|
||||
;
|
||||
|
||||
|
||||
terminal NUMBER returns number: /(0|[1-9][0-9]*)(\.[0-9]+)?/;
|
||||
|
||||
terminal BOOLEAN returns boolean: 'true' | 'false';
|
||||
|
||||
terminal GRATICULE returns string: 'circle' | 'polygon';
|
||||
|
||||
terminal ID returns string: /[a-zA-Z_][a-zA-Z0-9\-_]*/;
|
||||
terminal STRING: /"[^"]*"|'[^']*'/;
|
||||
terminal GRATICULE returns string: 'circle' | 'polygon';
|
||||
88
packages/parser/tests/architecture.test.ts
Normal file
88
packages/parser/tests/architecture.test.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { Architecture } from '../src/language/index.js';
|
||||
import { expectNoErrorsOrAlternatives, architectureParse as parse } from './test-util.js';
|
||||
|
||||
describe('architecture', () => {
|
||||
describe('should handle architecture definition', () => {
|
||||
it.each([
|
||||
`architecture-beta`,
|
||||
` architecture-beta `,
|
||||
`\tarchitecture-beta\t`,
|
||||
`
|
||||
\tarchitecture-beta
|
||||
`,
|
||||
])('should handle regular architecture', (context: string) => {
|
||||
const result = parse(context);
|
||||
expectNoErrorsOrAlternatives(result);
|
||||
expect(result.value.$type).toBe(Architecture);
|
||||
});
|
||||
});
|
||||
|
||||
describe('should handle TitleAndAccessibilities', () => {
|
||||
it.each([
|
||||
`architecture-beta title sample title`,
|
||||
` architecture-beta title sample title `,
|
||||
`\tarchitecture-beta\ttitle sample title\t`,
|
||||
`architecture-beta
|
||||
\ttitle sample title
|
||||
`,
|
||||
])('should handle regular architecture + title in same line', (context: string) => {
|
||||
const result = parse(context);
|
||||
expectNoErrorsOrAlternatives(result);
|
||||
expect(result.value.$type).toBe(Architecture);
|
||||
|
||||
const { title } = result.value;
|
||||
expect(title).toBe('sample title');
|
||||
});
|
||||
|
||||
it.each([
|
||||
`architecture-beta
|
||||
title sample title`,
|
||||
`architecture-beta
|
||||
title sample title
|
||||
`,
|
||||
])('should handle regular architecture + title in next line', (context: string) => {
|
||||
const result = parse(context);
|
||||
expectNoErrorsOrAlternatives(result);
|
||||
expect(result.value.$type).toBe(Architecture);
|
||||
|
||||
const { title } = result.value;
|
||||
expect(title).toBe('sample title');
|
||||
});
|
||||
|
||||
it('should handle regular architecture + title + accTitle + accDescr', () => {
|
||||
const context = `architecture-beta
|
||||
title sample title
|
||||
accTitle: sample accTitle
|
||||
accDescr: sample accDescr
|
||||
`;
|
||||
const result = parse(context);
|
||||
expectNoErrorsOrAlternatives(result);
|
||||
expect(result.value.$type).toBe(Architecture);
|
||||
|
||||
const { title, accTitle, accDescr } = result.value;
|
||||
expect(title).toBe('sample title');
|
||||
expect(accTitle).toBe('sample accTitle');
|
||||
expect(accDescr).toBe('sample accDescr');
|
||||
});
|
||||
|
||||
it('should handle regular architecture + title + accTitle + multi-line accDescr', () => {
|
||||
const context = `architecture-beta
|
||||
title sample title
|
||||
accTitle: sample accTitle
|
||||
accDescr {
|
||||
sample accDescr
|
||||
}
|
||||
`;
|
||||
const result = parse(context);
|
||||
expectNoErrorsOrAlternatives(result);
|
||||
expect(result.value.$type).toBe(Architecture);
|
||||
|
||||
const { title, accTitle, accDescr } = result.value;
|
||||
expect(title).toBe('sample title');
|
||||
expect(accTitle).toBe('sample accTitle');
|
||||
expect(accDescr).toBe('sample accDescr');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -63,6 +63,12 @@ describe('Parsing Branch Statements', () => {
|
||||
expect(branch.name).toBe('master');
|
||||
});
|
||||
|
||||
it('should parse a branch name starting with numbers', () => {
|
||||
const result = parse(`gitGraph\n commit\n branch 1.0.1\n`);
|
||||
const branch = result.value.statements[1] as Branch;
|
||||
expect(branch.name).toBe('1.0.1');
|
||||
});
|
||||
|
||||
it('should parse a branch with an order property', () => {
|
||||
const result = parse(`gitGraph\n commit\n branch feature order:1\n`);
|
||||
const branch = result.value.statements[1] as Branch;
|
||||
|
||||
@@ -4,226 +4,247 @@ import { Pie } from '../src/language/index.js';
|
||||
import { expectNoErrorsOrAlternatives, pieParse as parse } from './test-util.js';
|
||||
|
||||
describe('pie', () => {
|
||||
it.each([
|
||||
`pie`,
|
||||
` pie `,
|
||||
`\tpie\t`,
|
||||
`
|
||||
describe('should handle pie definition with or without showData', () => {
|
||||
it.each([
|
||||
`pie`,
|
||||
` pie `,
|
||||
`\tpie\t`,
|
||||
`
|
||||
\tpie
|
||||
`,
|
||||
])('should handle regular pie', (context: string) => {
|
||||
const result = parse(context);
|
||||
expectNoErrorsOrAlternatives(result);
|
||||
expect(result.value.$type).toBe(Pie);
|
||||
});
|
||||
])('should handle regular pie', (context: string) => {
|
||||
const result = parse(context);
|
||||
expectNoErrorsOrAlternatives(result);
|
||||
expect(result.value.$type).toBe(Pie);
|
||||
});
|
||||
|
||||
it.each([
|
||||
`pie showData`,
|
||||
` pie showData `,
|
||||
`\tpie\tshowData\t`,
|
||||
`
|
||||
it.each([
|
||||
`pie showData`,
|
||||
` pie showData `,
|
||||
`\tpie\tshowData\t`,
|
||||
`
|
||||
pie\tshowData
|
||||
`,
|
||||
])('should handle regular showData', (context: string) => {
|
||||
const result = parse(context);
|
||||
expectNoErrorsOrAlternatives(result);
|
||||
expect(result.value.$type).toBe(Pie);
|
||||
])('should handle regular showData', (context: string) => {
|
||||
const result = parse(context);
|
||||
expectNoErrorsOrAlternatives(result);
|
||||
expect(result.value.$type).toBe(Pie);
|
||||
|
||||
const { showData } = result.value;
|
||||
expect(showData).toBeTruthy();
|
||||
const { showData } = result.value;
|
||||
expect(showData).toBeTruthy();
|
||||
});
|
||||
});
|
||||
describe('should handle TitleAndAccessibilities', () => {
|
||||
describe('should handle TitleAndAccessibilities without showData', () => {
|
||||
it.each([
|
||||
`pie title sample title`,
|
||||
` pie title sample title `,
|
||||
`\tpie\ttitle sample title\t`,
|
||||
`pie
|
||||
\ttitle sample title
|
||||
`,
|
||||
])('should handle regular pie + title in same line', (context: string) => {
|
||||
const result = parse(context);
|
||||
expectNoErrorsOrAlternatives(result);
|
||||
expect(result.value.$type).toBe(Pie);
|
||||
|
||||
it.each([
|
||||
`pie title sample title`,
|
||||
` pie title sample title `,
|
||||
`\tpie\ttitle sample title\t`,
|
||||
`pie
|
||||
\ttitle sample title
|
||||
`,
|
||||
])('should handle regular pie + title in same line', (context: string) => {
|
||||
const result = parse(context);
|
||||
expectNoErrorsOrAlternatives(result);
|
||||
expect(result.value.$type).toBe(Pie);
|
||||
const { title } = result.value;
|
||||
expect(title).toBe('sample title');
|
||||
});
|
||||
|
||||
const { title } = result.value;
|
||||
expect(title).toBe('sample title');
|
||||
});
|
||||
|
||||
it.each([
|
||||
`pie
|
||||
title sample title`,
|
||||
`pie
|
||||
title sample title
|
||||
`,
|
||||
`pie
|
||||
title sample title`,
|
||||
`pie
|
||||
title sample title
|
||||
`,
|
||||
])('should handle regular pie + title in different line', (context: string) => {
|
||||
const result = parse(context);
|
||||
expectNoErrorsOrAlternatives(result);
|
||||
expect(result.value.$type).toBe(Pie);
|
||||
|
||||
const { title } = result.value;
|
||||
expect(title).toBe('sample title');
|
||||
});
|
||||
|
||||
it.each([
|
||||
`pie showData title sample title`,
|
||||
`pie showData title sample title
|
||||
`,
|
||||
])('should handle regular pie + showData + title', (context: string) => {
|
||||
const result = parse(context);
|
||||
expectNoErrorsOrAlternatives(result);
|
||||
expect(result.value.$type).toBe(Pie);
|
||||
|
||||
const { showData, title } = result.value;
|
||||
expect(showData).toBeTruthy();
|
||||
expect(title).toBe('sample title');
|
||||
});
|
||||
|
||||
it.each([
|
||||
`pie showData
|
||||
title sample title`,
|
||||
`pie showData
|
||||
title sample title
|
||||
`,
|
||||
`pie showData
|
||||
title sample title`,
|
||||
`pie showData
|
||||
title sample title
|
||||
`,
|
||||
])('should handle regular showData + title in different line', (context: string) => {
|
||||
const result = parse(context);
|
||||
expectNoErrorsOrAlternatives(result);
|
||||
expect(result.value.$type).toBe(Pie);
|
||||
|
||||
const { showData, title } = result.value;
|
||||
expect(showData).toBeTruthy();
|
||||
expect(title).toBe('sample title');
|
||||
});
|
||||
|
||||
describe('sections', () => {
|
||||
describe('normal', () => {
|
||||
it.each([
|
||||
`pie
|
||||
title sample title`,
|
||||
`pie
|
||||
title sample title
|
||||
`,
|
||||
`pie
|
||||
title sample title`,
|
||||
`pie
|
||||
title sample title
|
||||
`,
|
||||
])('should handle regular pie + title in different line', (context: string) => {
|
||||
const result = parse(context);
|
||||
expectNoErrorsOrAlternatives(result);
|
||||
expect(result.value.$type).toBe(Pie);
|
||||
|
||||
const { title } = result.value;
|
||||
expect(title).toBe('sample title');
|
||||
});
|
||||
});
|
||||
|
||||
describe('should handle TitleAndAccessibilities with showData', () => {
|
||||
it.each([
|
||||
`pie showData title sample title`,
|
||||
`pie showData title sample title
|
||||
`,
|
||||
])('should handle regular pie + showData + title', (context: string) => {
|
||||
const result = parse(context);
|
||||
expectNoErrorsOrAlternatives(result);
|
||||
expect(result.value.$type).toBe(Pie);
|
||||
|
||||
const { showData, title } = result.value;
|
||||
expect(showData).toBeTruthy();
|
||||
expect(title).toBe('sample title');
|
||||
});
|
||||
|
||||
it.each([
|
||||
`pie showData
|
||||
title sample title`,
|
||||
`pie showData
|
||||
title sample title
|
||||
`,
|
||||
`pie showData
|
||||
title sample title`,
|
||||
`pie showData
|
||||
title sample title
|
||||
`,
|
||||
])('should handle regular showData + title in different line', (context: string) => {
|
||||
const result = parse(context);
|
||||
expectNoErrorsOrAlternatives(result);
|
||||
expect(result.value.$type).toBe(Pie);
|
||||
|
||||
const { showData, title } = result.value;
|
||||
expect(showData).toBeTruthy();
|
||||
expect(title).toBe('sample title');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('should handle sections', () => {
|
||||
it.each([
|
||||
`pie
|
||||
"GitHub":100
|
||||
"GitLab":50`,
|
||||
`pie
|
||||
`pie
|
||||
"GitHub" : 100
|
||||
"GitLab" : 50`,
|
||||
`pie
|
||||
`pie
|
||||
"GitHub"\t:\t100
|
||||
"GitLab"\t:\t50`,
|
||||
`pie
|
||||
`pie
|
||||
\t"GitHub" \t : \t 100
|
||||
\t"GitLab" \t : \t 50
|
||||
`,
|
||||
])('should handle regular sections', (context: string) => {
|
||||
const result = parse(context);
|
||||
expectNoErrorsOrAlternatives(result);
|
||||
expect(result.value.$type).toBe(Pie);
|
||||
])('should handle regular sections', (context: string) => {
|
||||
const result = parse(context);
|
||||
expectNoErrorsOrAlternatives(result);
|
||||
expect(result.value.$type).toBe(Pie);
|
||||
|
||||
const { sections } = result.value;
|
||||
expect(sections[0].label).toBe('GitHub');
|
||||
expect(sections[0].value).toBe(100);
|
||||
const { sections } = result.value;
|
||||
expect(sections[0].label).toBe('GitHub');
|
||||
expect(sections[0].value).toBe(100);
|
||||
|
||||
expect(sections[1].label).toBe('GitLab');
|
||||
expect(sections[1].value).toBe(50);
|
||||
});
|
||||
expect(sections[1].label).toBe('GitLab');
|
||||
expect(sections[1].value).toBe(50);
|
||||
});
|
||||
|
||||
it('should handle sections with showData', () => {
|
||||
const context = `pie showData
|
||||
it('should handle sections with showData', () => {
|
||||
const context = `pie showData
|
||||
"GitHub": 100
|
||||
"GitLab": 50`;
|
||||
const result = parse(context);
|
||||
expectNoErrorsOrAlternatives(result);
|
||||
expect(result.value.$type).toBe(Pie);
|
||||
const result = parse(context);
|
||||
expectNoErrorsOrAlternatives(result);
|
||||
expect(result.value.$type).toBe(Pie);
|
||||
|
||||
const { showData, sections } = result.value;
|
||||
expect(showData).toBeTruthy();
|
||||
const { showData, sections } = result.value;
|
||||
expect(showData).toBeTruthy();
|
||||
|
||||
expect(sections[0].label).toBe('GitHub');
|
||||
expect(sections[0].value).toBe(100);
|
||||
expect(sections[0].label).toBe('GitHub');
|
||||
expect(sections[0].value).toBe(100);
|
||||
|
||||
expect(sections[1].label).toBe('GitLab');
|
||||
expect(sections[1].value).toBe(50);
|
||||
});
|
||||
expect(sections[1].label).toBe('GitLab');
|
||||
expect(sections[1].value).toBe(50);
|
||||
});
|
||||
|
||||
it('should handle sections with title', () => {
|
||||
const context = `pie title sample wow
|
||||
it('should handle sections with title', () => {
|
||||
const context = `pie title sample wow
|
||||
"GitHub": 100
|
||||
"GitLab": 50`;
|
||||
const result = parse(context);
|
||||
expectNoErrorsOrAlternatives(result);
|
||||
expect(result.value.$type).toBe(Pie);
|
||||
const result = parse(context);
|
||||
expectNoErrorsOrAlternatives(result);
|
||||
expect(result.value.$type).toBe(Pie);
|
||||
|
||||
const { title, sections } = result.value;
|
||||
expect(title).toBe('sample wow');
|
||||
const { title, sections } = result.value;
|
||||
expect(title).toBe('sample wow');
|
||||
|
||||
expect(sections[0].label).toBe('GitHub');
|
||||
expect(sections[0].value).toBe(100);
|
||||
expect(sections[0].label).toBe('GitHub');
|
||||
expect(sections[0].value).toBe(100);
|
||||
|
||||
expect(sections[1].label).toBe('GitLab');
|
||||
expect(sections[1].value).toBe(50);
|
||||
});
|
||||
expect(sections[1].label).toBe('GitLab');
|
||||
expect(sections[1].value).toBe(50);
|
||||
});
|
||||
|
||||
it('should handle sections with accTitle', () => {
|
||||
const context = `pie accTitle: sample wow
|
||||
it('should handle value with positive decimal', () => {
|
||||
const context = `pie
|
||||
"ash": 60.67
|
||||
"bat": 40`;
|
||||
const result = parse(context);
|
||||
expectNoErrorsOrAlternatives(result);
|
||||
expect(result.value.$type).toBe(Pie);
|
||||
|
||||
const { sections } = result.value;
|
||||
expect(sections[0].label).toBe('ash');
|
||||
expect(sections[0].value).toBe(60.67);
|
||||
|
||||
expect(sections[1].label).toBe('bat');
|
||||
expect(sections[1].value).toBe(40);
|
||||
});
|
||||
|
||||
it('should handle sections with accTitle', () => {
|
||||
const context = `pie accTitle: sample wow
|
||||
"GitHub": 100
|
||||
"GitLab": 50`;
|
||||
const result = parse(context);
|
||||
expectNoErrorsOrAlternatives(result);
|
||||
expect(result.value.$type).toBe(Pie);
|
||||
const result = parse(context);
|
||||
expectNoErrorsOrAlternatives(result);
|
||||
expect(result.value.$type).toBe(Pie);
|
||||
|
||||
const { accTitle, sections } = result.value;
|
||||
expect(accTitle).toBe('sample wow');
|
||||
const { accTitle, sections } = result.value;
|
||||
expect(accTitle).toBe('sample wow');
|
||||
|
||||
expect(sections[0].label).toBe('GitHub');
|
||||
expect(sections[0].value).toBe(100);
|
||||
expect(sections[0].label).toBe('GitHub');
|
||||
expect(sections[0].value).toBe(100);
|
||||
|
||||
expect(sections[1].label).toBe('GitLab');
|
||||
expect(sections[1].value).toBe(50);
|
||||
});
|
||||
expect(sections[1].label).toBe('GitLab');
|
||||
expect(sections[1].value).toBe(50);
|
||||
});
|
||||
|
||||
it('should handle sections with single line accDescr', () => {
|
||||
const context = `pie accDescr: sample wow
|
||||
it('should handle sections with single line accDescr', () => {
|
||||
const context = `pie accDescr: sample wow
|
||||
"GitHub": 100
|
||||
"GitLab": 50`;
|
||||
const result = parse(context);
|
||||
expectNoErrorsOrAlternatives(result);
|
||||
expect(result.value.$type).toBe(Pie);
|
||||
const result = parse(context);
|
||||
expectNoErrorsOrAlternatives(result);
|
||||
expect(result.value.$type).toBe(Pie);
|
||||
|
||||
const { accDescr, sections } = result.value;
|
||||
expect(accDescr).toBe('sample wow');
|
||||
const { accDescr, sections } = result.value;
|
||||
expect(accDescr).toBe('sample wow');
|
||||
|
||||
expect(sections[0].label).toBe('GitHub');
|
||||
expect(sections[0].value).toBe(100);
|
||||
expect(sections[0].label).toBe('GitHub');
|
||||
expect(sections[0].value).toBe(100);
|
||||
|
||||
expect(sections[1].label).toBe('GitLab');
|
||||
expect(sections[1].value).toBe(50);
|
||||
});
|
||||
expect(sections[1].label).toBe('GitLab');
|
||||
expect(sections[1].value).toBe(50);
|
||||
});
|
||||
|
||||
it('should handle sections with multi line accDescr', () => {
|
||||
const context = `pie accDescr {
|
||||
it('should handle sections with multi line accDescr', () => {
|
||||
const context = `pie accDescr {
|
||||
sample wow
|
||||
}
|
||||
"GitHub": 100
|
||||
"GitLab": 50`;
|
||||
const result = parse(context);
|
||||
expectNoErrorsOrAlternatives(result);
|
||||
expect(result.value.$type).toBe(Pie);
|
||||
const result = parse(context);
|
||||
expectNoErrorsOrAlternatives(result);
|
||||
expect(result.value.$type).toBe(Pie);
|
||||
|
||||
const { accDescr, sections } = result.value;
|
||||
expect(accDescr).toBe('sample wow');
|
||||
const { accDescr, sections } = result.value;
|
||||
expect(accDescr).toBe('sample wow');
|
||||
|
||||
expect(sections[0].label).toBe('GitHub');
|
||||
expect(sections[0].value).toBe(100);
|
||||
expect(sections[0].label).toBe('GitHub');
|
||||
expect(sections[0].value).toBe(100);
|
||||
|
||||
expect(sections[1].label).toBe('GitLab');
|
||||
expect(sections[1].value).toBe(50);
|
||||
});
|
||||
expect(sections[1].label).toBe('GitLab');
|
||||
expect(sections[1].value).toBe(50);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import type { LangiumParser, ParseResult } from 'langium';
|
||||
import { expect, vi } from 'vitest';
|
||||
import type {
|
||||
Architecture,
|
||||
ArchitectureServices,
|
||||
Info,
|
||||
InfoServices,
|
||||
Pie,
|
||||
@@ -13,6 +15,7 @@ import type {
|
||||
GitGraphServices,
|
||||
} from '../src/language/index.js';
|
||||
import {
|
||||
createArchitectureServices,
|
||||
createInfoServices,
|
||||
createPieServices,
|
||||
createRadarServices,
|
||||
@@ -47,6 +50,17 @@ export function createInfoTestServices() {
|
||||
}
|
||||
export const infoParse = createInfoTestServices().parse;
|
||||
|
||||
const architectureServices: ArchitectureServices = createArchitectureServices().Architecture;
|
||||
const architectureParser: LangiumParser = architectureServices.parser.LangiumParser;
|
||||
export function createArchitectureTestServices() {
|
||||
const parse = (input: string) => {
|
||||
return architectureParser.parse<Architecture>(input);
|
||||
};
|
||||
|
||||
return { services: architectureServices, parse };
|
||||
}
|
||||
export const architectureParse = createArchitectureTestServices().parse;
|
||||
|
||||
const pieServices: PieServices = createPieServices().Pie;
|
||||
const pieParser: LangiumParser = pieServices.parser.LangiumParser;
|
||||
export function createPieTestServices() {
|
||||
|
||||
1358
pnpm-lock.yaml
generated
1358
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
115
scripts/compare-timings.ts
Normal file
115
scripts/compare-timings.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
/**
|
||||
* Compares new E2E test timings with previous timings and determines whether to keep the new timings.
|
||||
*
|
||||
* The script will:
|
||||
* 1. Read old timings from git HEAD
|
||||
* 2. Read new timings from the current file
|
||||
* 3. Compare the timings and specs
|
||||
* 4. Keep new timings if:
|
||||
* - Specs were added/removed
|
||||
* - Any timing changed by 20% or more
|
||||
* 5. Revert to old timings if:
|
||||
* - No significant timing changes
|
||||
*
|
||||
* This helps prevent unnecessary timing updates when test performance hasn't changed significantly.
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { execSync } from 'node:child_process';
|
||||
|
||||
interface Timing {
|
||||
spec: string;
|
||||
duration: number;
|
||||
}
|
||||
|
||||
interface TimingsFile {
|
||||
durations: Timing[];
|
||||
}
|
||||
|
||||
interface CleanupOptions {
|
||||
keepNew: boolean;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
const TIMINGS_FILE = 'cypress/timings.json';
|
||||
const TIMINGS_PATH = path.join(process.cwd(), TIMINGS_FILE);
|
||||
|
||||
function log(message: string): void {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(message);
|
||||
}
|
||||
|
||||
function readOldTimings(): TimingsFile {
|
||||
try {
|
||||
const oldContent = execSync(`git show HEAD:${TIMINGS_FILE}`, { encoding: 'utf8' });
|
||||
return JSON.parse(oldContent);
|
||||
} catch {
|
||||
log('Error getting old timings, using empty file');
|
||||
return { durations: [] };
|
||||
}
|
||||
}
|
||||
|
||||
function readNewTimings(): TimingsFile {
|
||||
return JSON.parse(fs.readFileSync(TIMINGS_PATH, 'utf8'));
|
||||
}
|
||||
|
||||
function cleanupFiles({ keepNew, reason }: CleanupOptions): void {
|
||||
if (keepNew) {
|
||||
log(`Keeping new timings: ${reason}`);
|
||||
} else {
|
||||
log(`Reverting to old timings: ${reason}`);
|
||||
execSync(`git checkout HEAD -- ${TIMINGS_FILE}`);
|
||||
}
|
||||
}
|
||||
|
||||
function compareTimings(): void {
|
||||
const oldTimings = readOldTimings();
|
||||
const newTimings = readNewTimings();
|
||||
|
||||
const oldSpecs = new Set(oldTimings.durations.map((d) => d.spec));
|
||||
const newSpecs = new Set(newTimings.durations.map((d) => d.spec));
|
||||
|
||||
// Check if specs were added or removed
|
||||
const addedSpecs = [...newSpecs].filter((spec) => !oldSpecs.has(spec));
|
||||
const removedSpecs = [...oldSpecs].filter((spec) => !newSpecs.has(spec));
|
||||
|
||||
if (addedSpecs.length > 0 || removedSpecs.length > 0) {
|
||||
log('Specs changed:');
|
||||
if (addedSpecs.length > 0) {
|
||||
log(`Added: ${addedSpecs.join(', ')}`);
|
||||
}
|
||||
if (removedSpecs.length > 0) {
|
||||
log(`Removed: ${removedSpecs.join(', ')}`);
|
||||
}
|
||||
return cleanupFiles({ keepNew: true, reason: 'Specs were added or removed' });
|
||||
}
|
||||
|
||||
// Check timing variations
|
||||
const timingChanges = newTimings.durations.map((newTiming) => {
|
||||
const oldTiming = oldTimings.durations.find((d) => d.spec === newTiming.spec);
|
||||
if (!oldTiming) {
|
||||
throw new Error(`Could not find old timing for spec: ${newTiming.spec}`);
|
||||
}
|
||||
const change = Math.abs(newTiming.duration - oldTiming.duration);
|
||||
const changePercent = change / oldTiming.duration;
|
||||
return { spec: newTiming.spec, change, changePercent };
|
||||
});
|
||||
|
||||
// Filter changes that's more than 5 seconds and 20% different
|
||||
const significantChanges = timingChanges.filter((t) => t.change > 5000 && t.changePercent >= 0.2);
|
||||
|
||||
if (significantChanges.length === 0) {
|
||||
log('No significant timing changes detected (threshold: 5s and 20%)');
|
||||
return cleanupFiles({ keepNew: false, reason: 'No significant timing changes' });
|
||||
}
|
||||
|
||||
log('Significant timing changes:');
|
||||
significantChanges.forEach((t) => {
|
||||
log(`${t.spec}: ${t.change.toFixed(1)}ms (${(t.changePercent * 100).toFixed(1)}%)`);
|
||||
});
|
||||
|
||||
cleanupFiles({ keepNew: true, reason: 'Significant timing changes detected' });
|
||||
}
|
||||
|
||||
compareTimings();
|
||||
Reference in New Issue
Block a user