mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-08-15 14:29:25 +02:00
Merge branch 'develop' into update-er-diagram
This commit is contained in:
703
cypress/integration/rendering/requirementDiagram-unified.spec.js
Normal file
703
cypress/integration/rendering/requirementDiagram-unified.spec.js
Normal file
@@ -0,0 +1,703 @@
|
||||
import { imgSnapshotTest, renderGraph } from '../../helpers/util.ts';
|
||||
|
||||
const testOptions = [
|
||||
{ description: '', options: { logLevel: 1 } },
|
||||
{ description: 'ELK: ', options: { logLevel: 1, layout: 'elk' } },
|
||||
{ description: 'HD: ', options: { logLevel: 1, look: 'handDrawn' } },
|
||||
];
|
||||
|
||||
describe('Requirement Diagram Unified', () => {
|
||||
testOptions.forEach(({ description, options }) => {
|
||||
it(`${description}should render a simple Requirement diagram`, () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
requirementDiagram
|
||||
requirement test_req {
|
||||
id: 1
|
||||
text: the test text.
|
||||
risk: high
|
||||
verifymethod: test
|
||||
}
|
||||
|
||||
element test_entity {
|
||||
type: simulation
|
||||
}
|
||||
|
||||
test_entity - satisfies -> test_req
|
||||
`,
|
||||
options
|
||||
);
|
||||
});
|
||||
|
||||
it(`${description}should render a simple Requirement diagram without htmlLabels`, () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
requirementDiagram
|
||||
requirement test_req {
|
||||
id: 1
|
||||
text: the test text.
|
||||
risk: high
|
||||
verifymethod: test
|
||||
}
|
||||
|
||||
element test_entity {
|
||||
type: simulation
|
||||
}
|
||||
|
||||
test_entity - satisfies -> test_req
|
||||
`,
|
||||
{ ...options, htmlLabels: false }
|
||||
);
|
||||
});
|
||||
|
||||
it(`${description}should render a not-so-simple Requirement diagram`, () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
requirementDiagram
|
||||
|
||||
requirement test_req {
|
||||
id: 1
|
||||
text: the test text.
|
||||
risk: high
|
||||
verifymethod: test
|
||||
}
|
||||
|
||||
functionalRequirement test_req2 {
|
||||
id: 1.1
|
||||
text: the second test text.
|
||||
risk: low
|
||||
verifymethod: inspection
|
||||
}
|
||||
|
||||
performanceRequirement test_req3 {
|
||||
id: 1.2
|
||||
text: the third test text.
|
||||
risk: medium
|
||||
verifymethod: demonstration
|
||||
}
|
||||
|
||||
interfaceRequirement test_req4 {
|
||||
id: 1.2.1
|
||||
text: the fourth test text.
|
||||
risk: medium
|
||||
verifymethod: analysis
|
||||
}
|
||||
|
||||
physicalRequirement test_req5 {
|
||||
id: 1.2.2
|
||||
text: the fifth test text.
|
||||
risk: medium
|
||||
verifymethod: analysis
|
||||
}
|
||||
|
||||
designConstraint test_req6 {
|
||||
id: 1.2.3
|
||||
text: the sixth test text.
|
||||
risk: medium
|
||||
verifymethod: analysis
|
||||
}
|
||||
|
||||
element test_entity {
|
||||
type: simulation
|
||||
}
|
||||
|
||||
element test_entity2 {
|
||||
type: word doc
|
||||
docRef: reqs/test_entity
|
||||
}
|
||||
|
||||
element test_entity3 {
|
||||
type: "test suite"
|
||||
docRef: github.com/all_the_tests
|
||||
}
|
||||
|
||||
|
||||
test_entity - satisfies -> test_req2
|
||||
test_req - traces -> test_req2
|
||||
test_req - contains -> test_req3
|
||||
test_req3 - contains -> test_req4
|
||||
test_req4 - derives -> test_req5
|
||||
test_req5 - refines -> test_req6
|
||||
test_entity3 - verifies -> test_req5
|
||||
test_req <- copies - test_entity2
|
||||
`,
|
||||
options
|
||||
);
|
||||
});
|
||||
|
||||
it(`${description}should render a not-so-simple Requirement diagram without htmlLabels`, () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
requirementDiagram
|
||||
|
||||
requirement test_req {
|
||||
id: 1
|
||||
text: the test text.
|
||||
risk: high
|
||||
verifymethod: test
|
||||
}
|
||||
|
||||
functionalRequirement test_req2 {
|
||||
id: 1.1
|
||||
text: the second test text.
|
||||
risk: low
|
||||
verifymethod: inspection
|
||||
}
|
||||
|
||||
performanceRequirement test_req3 {
|
||||
id: 1.2
|
||||
text: the third test text.
|
||||
risk: medium
|
||||
verifymethod: demonstration
|
||||
}
|
||||
|
||||
interfaceRequirement test_req4 {
|
||||
id: 1.2.1
|
||||
text: the fourth test text.
|
||||
risk: medium
|
||||
verifymethod: analysis
|
||||
}
|
||||
|
||||
physicalRequirement test_req5 {
|
||||
id: 1.2.2
|
||||
text: the fifth test text.
|
||||
risk: medium
|
||||
verifymethod: analysis
|
||||
}
|
||||
|
||||
designConstraint test_req6 {
|
||||
id: 1.2.3
|
||||
text: the sixth test text.
|
||||
risk: medium
|
||||
verifymethod: analysis
|
||||
}
|
||||
|
||||
element test_entity {
|
||||
type: simulation
|
||||
}
|
||||
|
||||
element test_entity2 {
|
||||
type: word doc
|
||||
docRef: reqs/test_entity
|
||||
}
|
||||
|
||||
element test_entity3 {
|
||||
type: "test suite"
|
||||
docRef: github.com/all_the_tests
|
||||
}
|
||||
|
||||
|
||||
test_entity - satisfies -> test_req2
|
||||
test_req - traces -> test_req2
|
||||
test_req - contains -> test_req3
|
||||
test_req3 - contains -> test_req4
|
||||
test_req4 - derives -> test_req5
|
||||
test_req5 - refines -> test_req6
|
||||
test_entity3 - verifies -> test_req5
|
||||
test_req <- copies - test_entity2
|
||||
`,
|
||||
{ ...options, htmlLabels: false }
|
||||
);
|
||||
});
|
||||
|
||||
it(`${description}should render multiple Requirement diagrams`, () => {
|
||||
imgSnapshotTest(
|
||||
[
|
||||
`
|
||||
requirementDiagram
|
||||
|
||||
requirement test_req {
|
||||
id: 1
|
||||
text: the test text.
|
||||
risk: high
|
||||
verifymethod: test
|
||||
}
|
||||
|
||||
element test_entity {
|
||||
type: simulation
|
||||
}
|
||||
|
||||
test_entity - satisfies -> test_req
|
||||
`,
|
||||
`
|
||||
requirementDiagram
|
||||
|
||||
requirement test_req {
|
||||
id: 1
|
||||
text: the test text.
|
||||
risk: high
|
||||
verifymethod: test
|
||||
}
|
||||
|
||||
element test_entity {
|
||||
type: simulation
|
||||
}
|
||||
|
||||
test_entity - satisfies -> test_req
|
||||
`,
|
||||
],
|
||||
options
|
||||
);
|
||||
});
|
||||
|
||||
it(`${description}should render a Requirement diagram with empty information`, () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
requirementDiagram
|
||||
requirement test_req {
|
||||
}
|
||||
element test_entity {
|
||||
}
|
||||
`,
|
||||
options
|
||||
);
|
||||
});
|
||||
|
||||
it(`${description}should render requirements and elements with and without information`, () => {
|
||||
renderGraph(
|
||||
`
|
||||
requirementDiagram
|
||||
requirement test_req {
|
||||
id: 1
|
||||
text: the test text.
|
||||
risk: high
|
||||
verifymethod: test
|
||||
}
|
||||
element test_entity {
|
||||
}
|
||||
`,
|
||||
options
|
||||
);
|
||||
});
|
||||
|
||||
it(`${description}should render requirements and elements with long and short text`, () => {
|
||||
renderGraph(
|
||||
`
|
||||
requirementDiagram
|
||||
requirement test_req {
|
||||
id: 1
|
||||
text: the test text that is long and takes up a lot of space.
|
||||
risk: high
|
||||
verifymethod: test
|
||||
}
|
||||
element test_entity_name_that_is_extra_long {
|
||||
}
|
||||
`,
|
||||
options
|
||||
);
|
||||
});
|
||||
|
||||
it(`${description}should render requirements and elements with long and short text without htmlLabels`, () => {
|
||||
renderGraph(
|
||||
`
|
||||
requirementDiagram
|
||||
requirement test_req {
|
||||
id: 1
|
||||
text: the test text that is long and takes up a lot of space.
|
||||
risk: high
|
||||
verifymethod: test
|
||||
}
|
||||
element test_entity_name_that_is_extra_long {
|
||||
}
|
||||
`,
|
||||
{ ...options, htmlLabels: false }
|
||||
);
|
||||
});
|
||||
|
||||
it(`${description}should render requirements and elements with quoted text for spaces`, () => {
|
||||
renderGraph(
|
||||
`
|
||||
requirementDiagram
|
||||
requirement "test req name with spaces" {
|
||||
id: 1
|
||||
text: the test text that is long and takes up a lot of space.
|
||||
risk: high
|
||||
verifymethod: test
|
||||
}
|
||||
element "test entity name that is extra long with spaces" {
|
||||
}
|
||||
`,
|
||||
options
|
||||
);
|
||||
});
|
||||
|
||||
it(`${description}should render requirements and elements with markdown text`, () => {
|
||||
renderGraph(
|
||||
`
|
||||
requirementDiagram
|
||||
requirement "__my bolded name__" {
|
||||
id: 1
|
||||
text: "**Bolded text** _italicized text_"
|
||||
risk: high
|
||||
verifymethod: test
|
||||
}
|
||||
element "*my italicized name*" {
|
||||
type: "**Bolded type** _italicized type_"
|
||||
docref: "*Italicized* __Bolded__"
|
||||
}
|
||||
`,
|
||||
options
|
||||
);
|
||||
});
|
||||
|
||||
it(`${description}should render requirements and elements with markdown text without htmlLabels`, () => {
|
||||
renderGraph(
|
||||
`
|
||||
requirementDiagram
|
||||
requirement "__my bolded name__" {
|
||||
id: 1
|
||||
text: "**Bolded text** _italicized text_"
|
||||
risk: high
|
||||
verifymethod: test
|
||||
}
|
||||
element "*my italicized name*" {
|
||||
type: "**Bolded type** _italicized type_"
|
||||
docref: "*Italicized* __Bolded__"
|
||||
}
|
||||
`,
|
||||
{ ...options, htmlLabels: false }
|
||||
);
|
||||
});
|
||||
|
||||
it(`${description}should render a simple Requirement diagram with a title`, () => {
|
||||
imgSnapshotTest(
|
||||
`---
|
||||
title: simple Requirement diagram
|
||||
---
|
||||
requirementDiagram
|
||||
|
||||
requirement test_req {
|
||||
id: 1
|
||||
text: the test text.
|
||||
risk: high
|
||||
verifymethod: test
|
||||
}
|
||||
|
||||
element test_entity {
|
||||
type: simulation
|
||||
}
|
||||
|
||||
test_entity - satisfies -> test_req
|
||||
`,
|
||||
options
|
||||
);
|
||||
});
|
||||
|
||||
it(`${description}should render a Requirement diagram with TB direction`, () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
requirementDiagram
|
||||
direction TB
|
||||
|
||||
requirement test_req {
|
||||
id: 1
|
||||
text: the test text.
|
||||
risk: high
|
||||
verifymethod: test
|
||||
}
|
||||
|
||||
element test_entity {
|
||||
type: simulation
|
||||
}
|
||||
|
||||
test_entity - satisfies -> test_req
|
||||
`,
|
||||
options
|
||||
);
|
||||
});
|
||||
|
||||
it(`${description}should render a Requirement diagram with BT direction`, () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
requirementDiagram
|
||||
direction BT
|
||||
|
||||
requirement test_req {
|
||||
id: 1
|
||||
text: the test text.
|
||||
risk: high
|
||||
verifymethod: test
|
||||
}
|
||||
|
||||
element test_entity {
|
||||
type: simulation
|
||||
}
|
||||
|
||||
test_entity - satisfies -> test_req
|
||||
`,
|
||||
options
|
||||
);
|
||||
});
|
||||
|
||||
it(`${description}should render a Requirement diagram with LR direction`, () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
requirementDiagram
|
||||
direction LR
|
||||
|
||||
requirement test_req {
|
||||
id: 1
|
||||
text: the test text.
|
||||
risk: high
|
||||
verifymethod: test
|
||||
}
|
||||
|
||||
element test_entity {
|
||||
type: simulation
|
||||
}
|
||||
|
||||
test_entity - satisfies -> test_req
|
||||
`,
|
||||
options
|
||||
);
|
||||
});
|
||||
|
||||
it(`${description}should render a Requirement diagram with RL direction`, () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
requirementDiagram
|
||||
direction RL
|
||||
|
||||
requirement test_req {
|
||||
id: 1
|
||||
text: the test text.
|
||||
risk: high
|
||||
verifymethod: test
|
||||
}
|
||||
|
||||
element test_entity {
|
||||
type: simulation
|
||||
}
|
||||
|
||||
test_entity - satisfies -> test_req
|
||||
`,
|
||||
options
|
||||
);
|
||||
});
|
||||
|
||||
it(`${description}should render requirements and elements with styles applied from style statement`, () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
requirementDiagram
|
||||
|
||||
requirement test_req {
|
||||
id: 1
|
||||
text: the test text.
|
||||
risk: high
|
||||
verifymethod: test
|
||||
}
|
||||
|
||||
element test_entity {
|
||||
type: simulation
|
||||
}
|
||||
|
||||
test_entity - satisfies -> test_req
|
||||
|
||||
style test_req,test_entity fill:#f9f,stroke:blue, color:grey, font-weight:bold
|
||||
`,
|
||||
options
|
||||
);
|
||||
});
|
||||
|
||||
it(`${description}should render requirements and elements with styles applied from style statement without htmlLabels`, () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
requirementDiagram
|
||||
|
||||
requirement test_req {
|
||||
id: 1
|
||||
text: the test text.
|
||||
risk: high
|
||||
verifymethod: test
|
||||
}
|
||||
|
||||
element test_entity {
|
||||
type: simulation
|
||||
}
|
||||
|
||||
test_entity - satisfies -> test_req
|
||||
|
||||
style test_req,test_entity fill:#f9f,stroke:blue, color:grey, font-weight:bold
|
||||
`,
|
||||
{ ...options, htmlLabels: false }
|
||||
);
|
||||
});
|
||||
|
||||
it(`${description}should render requirements and elements with styles applied from class statement`, () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
requirementDiagram
|
||||
|
||||
requirement test_req {
|
||||
id: 1
|
||||
text: the test text.
|
||||
risk: high
|
||||
verifymethod: test
|
||||
}
|
||||
|
||||
element test_entity {
|
||||
type: simulation
|
||||
}
|
||||
|
||||
test_entity - satisfies -> test_req
|
||||
classDef bold font-weight: bold
|
||||
classDef blue stroke:lightblue, color: #0000FF
|
||||
class test_entity bold
|
||||
class test_req blue, bold
|
||||
`,
|
||||
options
|
||||
);
|
||||
});
|
||||
|
||||
it(`${description}should render requirements and elements with styles applied from class statement without htmlLabels`, () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
requirementDiagram
|
||||
|
||||
requirement test_req {
|
||||
id: 1
|
||||
text: the test text.
|
||||
risk: high
|
||||
verifymethod: test
|
||||
}
|
||||
|
||||
element test_entity {
|
||||
type: simulation
|
||||
}
|
||||
|
||||
test_entity - satisfies -> test_req
|
||||
classDef bold font-weight: bold
|
||||
classDef blue stroke:lightblue, color: #0000FF
|
||||
class test_entity bold
|
||||
class test_req blue, bold
|
||||
`,
|
||||
{ ...options, htmlLabels: false }
|
||||
);
|
||||
});
|
||||
|
||||
it(`${description}should render requirements and elements with styles applied from classes with shorthand syntax`, () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
requirementDiagram
|
||||
|
||||
requirement test_req:::blue {
|
||||
id: 1
|
||||
text: the test text.
|
||||
risk: high
|
||||
verifymethod: test
|
||||
}
|
||||
|
||||
element test_entity {
|
||||
type: simulation
|
||||
}
|
||||
|
||||
test_entity - satisfies -> test_req
|
||||
classDef bold font-weight: bold
|
||||
classDef blue stroke:lightblue, color: #0000FF
|
||||
test_entity:::bold
|
||||
`,
|
||||
options
|
||||
);
|
||||
});
|
||||
|
||||
it(`${description}should render requirements and elements with styles applied from classes with shorthand syntax without htmlLabels`, () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
requirementDiagram
|
||||
|
||||
requirement test_req:::blue {
|
||||
id: 1
|
||||
text: the test text.
|
||||
risk: high
|
||||
verifymethod: test
|
||||
}
|
||||
|
||||
element test_entity {
|
||||
type: simulation
|
||||
}
|
||||
|
||||
test_entity - satisfies -> test_req
|
||||
classDef bold font-weight: bold
|
||||
classDef blue stroke:lightblue, color: #0000FF
|
||||
test_entity:::bold
|
||||
`,
|
||||
{ ...options, htmlLabels: false }
|
||||
);
|
||||
});
|
||||
|
||||
it(`${description}should render requirements and elements with styles applied from the default class and other styles`, () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
requirementDiagram
|
||||
|
||||
requirement test_req:::blue {
|
||||
id: 1
|
||||
text: the test text.
|
||||
risk: high
|
||||
verifymethod: test
|
||||
}
|
||||
|
||||
element test_entity {
|
||||
type: simulation
|
||||
}
|
||||
|
||||
test_entity - satisfies -> test_req
|
||||
classDef blue stroke:lightblue, color:blue
|
||||
classDef default fill:pink
|
||||
style test_entity color:green
|
||||
`,
|
||||
options
|
||||
);
|
||||
});
|
||||
|
||||
it(`${description}should render requirements and elements with styles applied from the default class and other styles without htmlLabels`, () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
requirementDiagram
|
||||
|
||||
requirement test_req:::blue {
|
||||
id: 1
|
||||
text: the test text.
|
||||
risk: high
|
||||
verifymethod: test
|
||||
}
|
||||
|
||||
element test_entity {
|
||||
type: simulation
|
||||
}
|
||||
|
||||
test_entity - satisfies -> test_req
|
||||
classDef blue stroke:lightblue, color:blue
|
||||
classDef default fill:pink
|
||||
style test_entity color:green
|
||||
`,
|
||||
{ ...options, htmlLabels: false }
|
||||
);
|
||||
});
|
||||
|
||||
it(`${description}should render a Requirement diagram with a theme`, () => {
|
||||
imgSnapshotTest(
|
||||
`
|
||||
---
|
||||
theme: forest
|
||||
---
|
||||
requirementDiagram
|
||||
|
||||
requirement test_req:::blue {
|
||||
id: 1
|
||||
text: the test text.
|
||||
risk: high
|
||||
verifymethod: test
|
||||
}
|
||||
|
||||
element test_entity {
|
||||
type: simulation
|
||||
}
|
||||
|
||||
test_entity - satisfies -> test_req
|
||||
`,
|
||||
options
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
@@ -10,7 +10,7 @@
|
||||
|
||||
# Interface: DetailedError
|
||||
|
||||
Defined in: [packages/mermaid/src/utils.ts:780](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/utils.ts#L780)
|
||||
Defined in: [packages/mermaid/src/utils.ts:783](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/utils.ts#L783)
|
||||
|
||||
## Properties
|
||||
|
||||
@@ -18,7 +18,7 @@ Defined in: [packages/mermaid/src/utils.ts:780](https://github.com/mermaid-js/me
|
||||
|
||||
> `optional` **error**: `any`
|
||||
|
||||
Defined in: [packages/mermaid/src/utils.ts:785](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/utils.ts#L785)
|
||||
Defined in: [packages/mermaid/src/utils.ts:788](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/utils.ts#L788)
|
||||
|
||||
---
|
||||
|
||||
@@ -26,7 +26,7 @@ Defined in: [packages/mermaid/src/utils.ts:785](https://github.com/mermaid-js/me
|
||||
|
||||
> **hash**: `any`
|
||||
|
||||
Defined in: [packages/mermaid/src/utils.ts:783](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/utils.ts#L783)
|
||||
Defined in: [packages/mermaid/src/utils.ts:786](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/utils.ts#L786)
|
||||
|
||||
---
|
||||
|
||||
@@ -34,7 +34,7 @@ Defined in: [packages/mermaid/src/utils.ts:783](https://github.com/mermaid-js/me
|
||||
|
||||
> `optional` **message**: `string`
|
||||
|
||||
Defined in: [packages/mermaid/src/utils.ts:786](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/utils.ts#L786)
|
||||
Defined in: [packages/mermaid/src/utils.ts:789](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/utils.ts#L789)
|
||||
|
||||
---
|
||||
|
||||
@@ -42,4 +42,4 @@ Defined in: [packages/mermaid/src/utils.ts:786](https://github.com/mermaid-js/me
|
||||
|
||||
> **str**: `string`
|
||||
|
||||
Defined in: [packages/mermaid/src/utils.ts:781](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/utils.ts#L781)
|
||||
Defined in: [packages/mermaid/src/utils.ts:784](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/utils.ts#L784)
|
||||
|
@@ -84,6 +84,37 @@ element user_defined_name {
|
||||
}
|
||||
```
|
||||
|
||||
### Markdown Formatting
|
||||
|
||||
In places where user defined text is possible (like names, requirement text, element docref, etc.), you can:
|
||||
|
||||
- Surround the text in quotes: `"example text"`
|
||||
- Use markdown formatting inside quotes: `"**bold text** and *italics*"`
|
||||
|
||||
Example:
|
||||
|
||||
```mermaid-example
|
||||
requirementDiagram
|
||||
|
||||
requirement "__test_req__" {
|
||||
id: 1
|
||||
text: "*italicized text* **bold text**"
|
||||
risk: high
|
||||
verifymethod: test
|
||||
}
|
||||
```
|
||||
|
||||
```mermaid
|
||||
requirementDiagram
|
||||
|
||||
requirement "__test_req__" {
|
||||
id: 1
|
||||
text: "*italicized text* **bold text**"
|
||||
risk: high
|
||||
verifymethod: test
|
||||
}
|
||||
```
|
||||
|
||||
### Relationship
|
||||
|
||||
Relationships are comprised of a source node, destination node, and relationship type.
|
||||
@@ -250,4 +281,215 @@ This example uses all features of the diagram.
|
||||
test_req <- copies - test_entity2
|
||||
```
|
||||
|
||||
## Direction
|
||||
|
||||
The diagram can be rendered in different directions using the `direction` statement. Valid values are:
|
||||
|
||||
- `TB` - Top to Bottom (default)
|
||||
- `BT` - Bottom to Top
|
||||
- `LR` - Left to Right
|
||||
- `RL` - Right to Left
|
||||
|
||||
Example:
|
||||
|
||||
```mermaid-example
|
||||
requirementDiagram
|
||||
|
||||
direction LR
|
||||
|
||||
requirement test_req {
|
||||
id: 1
|
||||
text: the test text.
|
||||
risk: high
|
||||
verifymethod: test
|
||||
}
|
||||
|
||||
element test_entity {
|
||||
type: simulation
|
||||
}
|
||||
|
||||
test_entity - satisfies -> test_req
|
||||
```
|
||||
|
||||
```mermaid
|
||||
requirementDiagram
|
||||
|
||||
direction LR
|
||||
|
||||
requirement test_req {
|
||||
id: 1
|
||||
text: the test text.
|
||||
risk: high
|
||||
verifymethod: test
|
||||
}
|
||||
|
||||
element test_entity {
|
||||
type: simulation
|
||||
}
|
||||
|
||||
test_entity - satisfies -> test_req
|
||||
```
|
||||
|
||||
## Styling
|
||||
|
||||
Requirements and elements can be styled using direct styling or classes. As a rule of thumb, when applying styles or classes, it accepts a list of requirement or element names and a list of class names allowing multiple assignments at a time (The only exception is the shorthand syntax `:::` which can assign multiple classes but only to one requirement or element at a time).
|
||||
|
||||
### Direct Styling
|
||||
|
||||
Use the `style` keyword to apply CSS styles directly:
|
||||
|
||||
```mermaid-example
|
||||
requirementDiagram
|
||||
|
||||
requirement test_req {
|
||||
id: 1
|
||||
text: styling example
|
||||
risk: low
|
||||
verifymethod: test
|
||||
}
|
||||
|
||||
element test_entity {
|
||||
type: simulation
|
||||
}
|
||||
|
||||
style test_req fill:#ffa,stroke:#000, color: green
|
||||
style test_entity fill:#f9f,stroke:#333, color: blue
|
||||
```
|
||||
|
||||
```mermaid
|
||||
requirementDiagram
|
||||
|
||||
requirement test_req {
|
||||
id: 1
|
||||
text: styling example
|
||||
risk: low
|
||||
verifymethod: test
|
||||
}
|
||||
|
||||
element test_entity {
|
||||
type: simulation
|
||||
}
|
||||
|
||||
style test_req fill:#ffa,stroke:#000, color: green
|
||||
style test_entity fill:#f9f,stroke:#333, color: blue
|
||||
```
|
||||
|
||||
### Class Definitions
|
||||
|
||||
Define reusable styles using `classDef`:
|
||||
|
||||
```mermaid-example
|
||||
requirementDiagram
|
||||
|
||||
requirement test_req {
|
||||
id: 1
|
||||
text: "class styling example"
|
||||
risk: low
|
||||
verifymethod: test
|
||||
}
|
||||
|
||||
element test_entity {
|
||||
type: simulation
|
||||
}
|
||||
|
||||
classDef important fill:#f96,stroke:#333,stroke-width:4px
|
||||
classDef test fill:#ffa,stroke:#000
|
||||
```
|
||||
|
||||
```mermaid
|
||||
requirementDiagram
|
||||
|
||||
requirement test_req {
|
||||
id: 1
|
||||
text: "class styling example"
|
||||
risk: low
|
||||
verifymethod: test
|
||||
}
|
||||
|
||||
element test_entity {
|
||||
type: simulation
|
||||
}
|
||||
|
||||
classDef important fill:#f96,stroke:#333,stroke-width:4px
|
||||
classDef test fill:#ffa,stroke:#000
|
||||
```
|
||||
|
||||
### Default class
|
||||
|
||||
If a class is named default it will be applied to all nodes. Specific styles and classes should be defined afterwards to override the applied default styling.
|
||||
|
||||
```
|
||||
classDef default fill:#f9f,stroke:#333,stroke-width:4px;
|
||||
```
|
||||
|
||||
### Applying Classes
|
||||
|
||||
Classes can be applied in two ways:
|
||||
|
||||
1. Using the `class` keyword:
|
||||
|
||||
```
|
||||
class test_req,test_entity important
|
||||
```
|
||||
|
||||
2. Using the shorthand syntax with `:::` either during the definition or afterwards:
|
||||
|
||||
```
|
||||
requirement test_req:::important {
|
||||
id: 1
|
||||
text: class styling example
|
||||
risk: low
|
||||
verifymethod: test
|
||||
}
|
||||
```
|
||||
|
||||
```
|
||||
element test_elem {
|
||||
}
|
||||
|
||||
test_elem:::myClass
|
||||
```
|
||||
|
||||
### Combined Example
|
||||
|
||||
```mermaid-example
|
||||
requirementDiagram
|
||||
|
||||
requirement test_req:::important {
|
||||
id: 1
|
||||
text: "class styling example"
|
||||
risk: low
|
||||
verifymethod: test
|
||||
}
|
||||
|
||||
element test_entity {
|
||||
type: simulation
|
||||
}
|
||||
|
||||
classDef important font-weight:bold
|
||||
|
||||
class test_entity important
|
||||
style test_entity fill:#f9f,stroke:#333
|
||||
```
|
||||
|
||||
```mermaid
|
||||
requirementDiagram
|
||||
|
||||
requirement test_req:::important {
|
||||
id: 1
|
||||
text: "class styling example"
|
||||
risk: low
|
||||
verifymethod: test
|
||||
}
|
||||
|
||||
element test_entity {
|
||||
type: simulation
|
||||
}
|
||||
|
||||
classDef important font-weight:bold
|
||||
|
||||
class test_entity important
|
||||
style test_entity fill:#f9f,stroke:#333
|
||||
```
|
||||
|
||||
<!--- cspell:ignore reqs --->
|
||||
|
@@ -9,6 +9,7 @@
|
||||
%x string
|
||||
%x token
|
||||
%x unqString
|
||||
%x style
|
||||
%x acc_title
|
||||
%x acc_descr
|
||||
%x acc_descr_multiline
|
||||
@@ -22,6 +23,10 @@ accDescr\s*":"\s* { this.begin("ac
|
||||
accDescr\s*"{"\s* { this.begin("acc_descr_multiline");}
|
||||
<acc_descr_multiline>[\}] { this.popState(); }
|
||||
<acc_descr_multiline>[^\}]* return "acc_descr_multiline_value";
|
||||
.*direction\s+TB[^\n]* return 'direction_tb';
|
||||
.*direction\s+BT[^\n]* return 'direction_bt';
|
||||
.*direction\s+RL[^\n]* return 'direction_rl';
|
||||
.*direction\s+LR[^\n]* return 'direction_lr';
|
||||
(\r?\n)+ return 'NEWLINE';
|
||||
\s+ /* skip all whitespace */
|
||||
\#[^\n]* /* skip comments */
|
||||
@@ -32,6 +37,7 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multili
|
||||
|
||||
"{" return 'STRUCT_START';
|
||||
"}" return 'STRUCT_STOP';
|
||||
":"{3} return 'STYLE_SEPARATOR';
|
||||
":" return 'COLONSEP';
|
||||
|
||||
"id" return 'ID';
|
||||
@@ -68,6 +74,20 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multili
|
||||
"type" return 'TYPE';
|
||||
"docref" return 'DOCREF';
|
||||
|
||||
"style" { this.begin("style"); return 'STYLE'; }
|
||||
<style>\w+ return 'ALPHA';
|
||||
<style>":" return 'COLON';
|
||||
<style>";" return 'SEMICOLON';
|
||||
<style>"%" return 'PERCENT';
|
||||
<style>"-" return 'MINUS';
|
||||
<style>"#" return 'BRKT';
|
||||
<style>" " /* skip spaces */
|
||||
<style>["] { this.begin("string"); }
|
||||
<style>\n { this.popState(); }
|
||||
|
||||
"classDef" { this.begin("style"); return 'CLASSDEF'; }
|
||||
"class" { this.begin("style"); return 'CLASS'; }
|
||||
|
||||
"<-" return 'END_ARROW_L';
|
||||
"->" {return 'END_ARROW_R';}
|
||||
"-" {return 'LINE';}
|
||||
@@ -76,7 +96,11 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multili
|
||||
<string>["] { this.popState(); }
|
||||
<string>[^"]* { return "qString"; }
|
||||
|
||||
[\w][^\r\n\{\<\>\-\=]* { yytext = yytext.trim(); return 'unqString';}
|
||||
[\w][^:,\r\n\{\<\>\-\=]* { yytext = yytext.trim(); return 'unqString';}
|
||||
|
||||
<*>\w+ return 'ALPHA';
|
||||
<*>[0-9]+ return 'NUM';
|
||||
<*>"," return 'COMMA';
|
||||
|
||||
/lex
|
||||
|
||||
@@ -101,11 +125,28 @@ diagram
|
||||
| elementDef diagram
|
||||
| relationshipDef diagram
|
||||
| directive diagram
|
||||
| NEWLINE diagram;
|
||||
| direction diagram
|
||||
| styleStatement diagram
|
||||
| classDefStatement diagram
|
||||
| classStatement diagram
|
||||
| NEWLINE diagram
|
||||
;
|
||||
|
||||
direction
|
||||
: direction_tb
|
||||
{ yy.setDirection('TB');}
|
||||
| direction_bt
|
||||
{ yy.setDirection('BT');}
|
||||
| direction_rl
|
||||
{ yy.setDirection('RL');}
|
||||
| direction_lr
|
||||
{ yy.setDirection('LR');}
|
||||
;
|
||||
|
||||
requirementDef
|
||||
: requirementType requirementName STRUCT_START NEWLINE requirementBody
|
||||
{ yy.addRequirement($2, $1) };
|
||||
: requirementType requirementName STRUCT_START NEWLINE requirementBody { yy.addRequirement($2, $1) }
|
||||
| requirementType requirementName STYLE_SEPARATOR idList STRUCT_START NEWLINE requirementBody { yy.addRequirement($2, $1); yy.setClass([$2], $4); }
|
||||
;
|
||||
|
||||
requirementBody
|
||||
: ID COLONSEP id NEWLINE requirementBody
|
||||
@@ -149,8 +190,9 @@ verifyType
|
||||
{ $$=yy.VerifyType.VERIFY_TEST;};
|
||||
|
||||
elementDef
|
||||
: ELEMENT elementName STRUCT_START NEWLINE elementBody
|
||||
{ yy.addElement($2) };
|
||||
: ELEMENT elementName STRUCT_START NEWLINE elementBody { yy.addElement($2) }
|
||||
| ELEMENT elementName STYLE_SEPARATOR idList STRUCT_START NEWLINE elementBody { yy.addElement($2); yy.setClass([$2], $4); }
|
||||
;
|
||||
|
||||
elementBody
|
||||
: TYPE COLONSEP type NEWLINE elementBody
|
||||
@@ -182,6 +224,38 @@ relationship
|
||||
| TRACES
|
||||
{ $$=yy.Relationships.TRACES;};
|
||||
|
||||
classDefStatement
|
||||
: CLASSDEF idList stylesOpt {$$ = $CLASSDEF;yy.defineClass($idList,$stylesOpt);}
|
||||
;
|
||||
|
||||
classStatement
|
||||
: CLASS idList idList {yy.setClass($2, $3);}
|
||||
| id STYLE_SEPARATOR idList {yy.setClass([$1], $3);}
|
||||
;
|
||||
|
||||
idList
|
||||
: ALPHA { $$ = [$ALPHA]; }
|
||||
| idList COMMA ALPHA = { $$ = $idList.concat([$ALPHA]); }
|
||||
| id { $$ = [$id]; }
|
||||
| idList COMMA id = { $$ = $idList.concat([$id]); }
|
||||
;
|
||||
|
||||
styleStatement
|
||||
: STYLE idList stylesOpt {$$ = $STYLE;yy.setCssStyle($2,$stylesOpt);}
|
||||
;
|
||||
|
||||
stylesOpt
|
||||
: style {$$ = [$style]}
|
||||
| stylesOpt COMMA style {$stylesOpt.push($style);$$ = $stylesOpt;}
|
||||
;
|
||||
|
||||
style
|
||||
: styleComponent
|
||||
| style styleComponent {$$ = $style + $styleComponent;}
|
||||
;
|
||||
|
||||
styleComponent: ALPHA | NUM | COLON | UNIT | SPACE | BRKT | PCT | MINUS | LABEL | SEMICOLON;
|
||||
|
||||
|
||||
requirementName: unqString | qString;
|
||||
id : unqString | qString;
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { setConfig } from '../../../config.js';
|
||||
import requirementDb from '../requirementDb.js';
|
||||
import { RequirementDB } from '../requirementDb.js';
|
||||
import reqDiagram from './requirementDiagram.jison';
|
||||
|
||||
setConfig({
|
||||
@@ -7,6 +7,7 @@ setConfig({
|
||||
});
|
||||
|
||||
describe('when parsing requirement diagram it...', function () {
|
||||
const requirementDb = new RequirementDB();
|
||||
beforeEach(function () {
|
||||
reqDiagram.parser.yy = requirementDb;
|
||||
reqDiagram.parser.yy.clear();
|
||||
@@ -37,7 +38,7 @@ describe('when parsing requirement diagram it...', function () {
|
||||
|
||||
let foundReq = requirementDb.getRequirements().get(expectedName);
|
||||
expect(foundReq).toBeDefined();
|
||||
expect(foundReq.id).toBe(expectedId);
|
||||
expect(foundReq.requirementId).toBe(expectedId);
|
||||
expect(foundReq.text).toBe(expectedText);
|
||||
|
||||
expect(requirementDb.getElements().size).toBe(0);
|
||||
@@ -599,4 +600,251 @@ line 2`;
|
||||
expect(reqDiagram.parser.yy.getElements().size).toBe(1);
|
||||
});
|
||||
}
|
||||
|
||||
it('will accept styling a requirement', function () {
|
||||
const expectedName = 'test_req';
|
||||
|
||||
let lines = [
|
||||
`requirementDiagram`,
|
||||
``,
|
||||
`requirement ${expectedName} {`,
|
||||
`}`,
|
||||
`style ${expectedName} fill:#f9f,stroke:#333,stroke-width:4px`,
|
||||
``,
|
||||
];
|
||||
let doc = lines.join('\n');
|
||||
|
||||
reqDiagram.parser.parse(doc);
|
||||
|
||||
let foundReq = requirementDb.getRequirements().get(expectedName);
|
||||
const styles = foundReq.cssStyles;
|
||||
expect(styles).toEqual(['fill:#f9f', 'stroke:#333', 'stroke-width:4px']);
|
||||
});
|
||||
|
||||
it('will accept styling an element', function () {
|
||||
const expectedName = 'test_element';
|
||||
|
||||
let lines = [
|
||||
`requirementDiagram`,
|
||||
``,
|
||||
`element ${expectedName} {`,
|
||||
`}`,
|
||||
`style ${expectedName} fill:#f9f,stroke:#333,stroke-width:4px`,
|
||||
``,
|
||||
];
|
||||
let doc = lines.join('\n');
|
||||
|
||||
reqDiagram.parser.parse(doc);
|
||||
|
||||
let foundElement = requirementDb.getElements().get(expectedName);
|
||||
const styles = foundElement.cssStyles;
|
||||
expect(styles).toEqual(['fill:#f9f', 'stroke:#333', 'stroke-width:4px']);
|
||||
});
|
||||
|
||||
it('will accept styling multiple things at once', function () {
|
||||
const expectedRequirementName = 'test_requirement';
|
||||
const expectedElementName = 'test_element';
|
||||
|
||||
let lines = [
|
||||
`requirementDiagram`,
|
||||
``,
|
||||
`requirement ${expectedRequirementName} {`,
|
||||
`}`,
|
||||
`element ${expectedElementName} {`,
|
||||
`}`,
|
||||
`style ${expectedRequirementName},${expectedElementName} fill:#f9f,stroke:#333,stroke-width:4px`,
|
||||
``,
|
||||
];
|
||||
let doc = lines.join('\n');
|
||||
|
||||
reqDiagram.parser.parse(doc);
|
||||
|
||||
let foundRequirement = requirementDb.getRequirements().get(expectedRequirementName);
|
||||
const requirementStyles = foundRequirement.cssStyles;
|
||||
expect(requirementStyles).toEqual(['fill:#f9f', 'stroke:#333', 'stroke-width:4px']);
|
||||
let foundElement = requirementDb.getElements().get(expectedElementName);
|
||||
const elementStyles = foundElement.cssStyles;
|
||||
expect(elementStyles).toEqual(['fill:#f9f', 'stroke:#333', 'stroke-width:4px']);
|
||||
});
|
||||
|
||||
it('will accept defining a class', function () {
|
||||
const expectedName = 'myClass';
|
||||
|
||||
let lines = [
|
||||
`requirementDiagram`,
|
||||
``,
|
||||
`classDef ${expectedName} fill:#f9f,stroke:#333,stroke-width:4px`,
|
||||
``,
|
||||
];
|
||||
let doc = lines.join('\n');
|
||||
|
||||
reqDiagram.parser.parse(doc);
|
||||
|
||||
let foundClass = requirementDb.getClasses().get(expectedName);
|
||||
expect(foundClass).toEqual({
|
||||
id: 'myClass',
|
||||
styles: ['fill:#f9f', 'stroke:#333', 'stroke-width:4px'],
|
||||
textStyles: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('will accept defining multiple classes at once', function () {
|
||||
const firstName = 'firstClass';
|
||||
const secondName = 'secondClass';
|
||||
|
||||
let lines = [
|
||||
`requirementDiagram`,
|
||||
``,
|
||||
`classDef ${firstName},${secondName} fill:#f9f,stroke:#333,stroke-width:4px`,
|
||||
``,
|
||||
];
|
||||
let doc = lines.join('\n');
|
||||
|
||||
reqDiagram.parser.parse(doc);
|
||||
|
||||
let firstClass = requirementDb.getClasses().get(firstName);
|
||||
expect(firstClass).toEqual({
|
||||
id: 'firstClass',
|
||||
styles: ['fill:#f9f', 'stroke:#333', 'stroke-width:4px'],
|
||||
textStyles: [],
|
||||
});
|
||||
let secondClass = requirementDb.getClasses().get(secondName);
|
||||
expect(secondClass).toEqual({
|
||||
id: 'secondClass',
|
||||
styles: ['fill:#f9f', 'stroke:#333', 'stroke-width:4px'],
|
||||
textStyles: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('will accept assigning a class via the class statement', function () {
|
||||
const requirementName = 'myReq';
|
||||
const className = 'myClass';
|
||||
|
||||
let lines = [
|
||||
`requirementDiagram`,
|
||||
``,
|
||||
`requirement ${requirementName} {`,
|
||||
`}`,
|
||||
`classDef ${className} fill:#f9f,stroke:#333,stroke-width:4px`,
|
||||
`class ${requirementName} ${className}`,
|
||||
``,
|
||||
];
|
||||
let doc = lines.join('\n');
|
||||
|
||||
reqDiagram.parser.parse(doc);
|
||||
|
||||
let foundRequirement = requirementDb.getRequirements().get(requirementName);
|
||||
expect(foundRequirement.classes).toEqual(['default', className]);
|
||||
});
|
||||
|
||||
it('will accept assigning multiple classes to multiple things via the class statement', function () {
|
||||
const requirementName = 'req';
|
||||
const elementName = 'elem';
|
||||
const firstClassName = 'class1';
|
||||
const secondClassName = 'class2';
|
||||
|
||||
let lines = [
|
||||
`requirementDiagram`,
|
||||
``,
|
||||
`requirement ${requirementName} {`,
|
||||
`}`,
|
||||
`element ${elementName} {`,
|
||||
`}`,
|
||||
`classDef ${firstClassName},${secondClassName} fill:#f9f,stroke:#333,stroke-width:4px`,
|
||||
`class ${requirementName},${elementName} ${firstClassName},${secondClassName}`,
|
||||
``,
|
||||
];
|
||||
let doc = lines.join('\n');
|
||||
|
||||
reqDiagram.parser.parse(doc);
|
||||
|
||||
let requirement = requirementDb.getRequirements().get(requirementName);
|
||||
expect(requirement.classes).toEqual(['default', firstClassName, secondClassName]);
|
||||
let element = requirementDb.getElements().get(elementName);
|
||||
expect(element.classes).toEqual(['default', firstClassName, secondClassName]);
|
||||
});
|
||||
|
||||
it('will accept assigning a class via the shorthand syntax', function () {
|
||||
const requirementName = 'myReq';
|
||||
const className = 'myClass';
|
||||
|
||||
let lines = [
|
||||
`requirementDiagram`,
|
||||
``,
|
||||
`requirement ${requirementName} {`,
|
||||
`}`,
|
||||
`classDef ${className} fill:#f9f,stroke:#333,stroke-width:4px`,
|
||||
`${requirementName}:::${className}`,
|
||||
``,
|
||||
];
|
||||
let doc = lines.join('\n');
|
||||
|
||||
reqDiagram.parser.parse(doc);
|
||||
|
||||
let foundRequirement = requirementDb.getRequirements().get(requirementName);
|
||||
expect(foundRequirement.classes).toEqual(['default', className]);
|
||||
});
|
||||
|
||||
it('will accept assigning multiple classes via the shorthand syntax', function () {
|
||||
const requirementName = 'myReq';
|
||||
const firstClassName = 'class1';
|
||||
const secondClassName = 'class2';
|
||||
|
||||
let lines = [
|
||||
`requirementDiagram`,
|
||||
``,
|
||||
`requirement ${requirementName} {`,
|
||||
`}`,
|
||||
`classDef ${firstClassName} fill:#f9f,stroke:#333,stroke-width:4px`,
|
||||
`classDef ${secondClassName} color:blue`,
|
||||
`${requirementName}:::${firstClassName},${secondClassName}`,
|
||||
``,
|
||||
];
|
||||
let doc = lines.join('\n');
|
||||
|
||||
reqDiagram.parser.parse(doc);
|
||||
|
||||
let foundRequirement = requirementDb.getRequirements().get(requirementName);
|
||||
expect(foundRequirement.classes).toEqual(['default', firstClassName, secondClassName]);
|
||||
});
|
||||
|
||||
it('will accept assigning a class or multiple via the shorthand syntax when defining a requirement or element', function () {
|
||||
const requirementName = 'myReq';
|
||||
const elementName = 'myElem';
|
||||
const firstClassName = 'class1';
|
||||
const secondClassName = 'class2';
|
||||
|
||||
let lines = [
|
||||
`requirementDiagram`,
|
||||
``,
|
||||
`requirement ${requirementName}:::${firstClassName} {`,
|
||||
`}`,
|
||||
`element ${elementName}:::${firstClassName},${secondClassName} {`,
|
||||
`}`,
|
||||
``,
|
||||
`classDef ${firstClassName} fill:#f9f,stroke:#333,stroke-width:4px`,
|
||||
`classDef ${secondClassName} color:blue`,
|
||||
``,
|
||||
];
|
||||
let doc = lines.join('\n');
|
||||
|
||||
reqDiagram.parser.parse(doc);
|
||||
|
||||
let foundRequirement = requirementDb.getRequirements().get(requirementName);
|
||||
expect(foundRequirement.classes).toEqual(['default', firstClassName]);
|
||||
let foundElement = requirementDb.getElements().get(elementName);
|
||||
expect(foundElement.classes).toEqual(['default', firstClassName, secondClassName]);
|
||||
});
|
||||
|
||||
describe('will parse direction statements and', () => {
|
||||
test.each(['TB', 'BT', 'LR', 'RL'])('will accept direction %s', (directionVal) => {
|
||||
const lines = ['requirementDiagram', '', `direction ${directionVal}`, ''];
|
||||
const doc = lines.join('\n');
|
||||
|
||||
reqDiagram.parser.parse(doc);
|
||||
|
||||
const direction = requirementDb.getDirection();
|
||||
expect(direction).toBe(directionVal);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -1,168 +0,0 @@
|
||||
import { getConfig } from '../../diagram-api/diagramAPI.js';
|
||||
import { log } from '../../logger.js';
|
||||
|
||||
import {
|
||||
setAccTitle,
|
||||
getAccTitle,
|
||||
getAccDescription,
|
||||
setAccDescription,
|
||||
clear as commonClear,
|
||||
} from '../common/commonDb.js';
|
||||
|
||||
let relations = [];
|
||||
let latestRequirement = {};
|
||||
let requirements = new Map();
|
||||
let latestElement = {};
|
||||
let elements = new Map();
|
||||
|
||||
const RequirementType = {
|
||||
REQUIREMENT: 'Requirement',
|
||||
FUNCTIONAL_REQUIREMENT: 'Functional Requirement',
|
||||
INTERFACE_REQUIREMENT: 'Interface Requirement',
|
||||
PERFORMANCE_REQUIREMENT: 'Performance Requirement',
|
||||
PHYSICAL_REQUIREMENT: 'Physical Requirement',
|
||||
DESIGN_CONSTRAINT: 'Design Constraint',
|
||||
};
|
||||
|
||||
const RiskLevel = {
|
||||
LOW_RISK: 'Low',
|
||||
MED_RISK: 'Medium',
|
||||
HIGH_RISK: 'High',
|
||||
};
|
||||
|
||||
const VerifyType = {
|
||||
VERIFY_ANALYSIS: 'Analysis',
|
||||
VERIFY_DEMONSTRATION: 'Demonstration',
|
||||
VERIFY_INSPECTION: 'Inspection',
|
||||
VERIFY_TEST: 'Test',
|
||||
};
|
||||
|
||||
const Relationships = {
|
||||
CONTAINS: 'contains',
|
||||
COPIES: 'copies',
|
||||
DERIVES: 'derives',
|
||||
SATISFIES: 'satisfies',
|
||||
VERIFIES: 'verifies',
|
||||
REFINES: 'refines',
|
||||
TRACES: 'traces',
|
||||
};
|
||||
|
||||
const addRequirement = (name, type) => {
|
||||
if (!requirements.has(name)) {
|
||||
requirements.set(name, {
|
||||
name,
|
||||
type,
|
||||
|
||||
id: latestRequirement.id,
|
||||
text: latestRequirement.text,
|
||||
risk: latestRequirement.risk,
|
||||
verifyMethod: latestRequirement.verifyMethod,
|
||||
});
|
||||
}
|
||||
latestRequirement = {};
|
||||
|
||||
return requirements.get(name);
|
||||
};
|
||||
|
||||
const getRequirements = () => requirements;
|
||||
|
||||
const setNewReqId = (id) => {
|
||||
if (latestRequirement !== undefined) {
|
||||
latestRequirement.id = id;
|
||||
}
|
||||
};
|
||||
|
||||
const setNewReqText = (text) => {
|
||||
if (latestRequirement !== undefined) {
|
||||
latestRequirement.text = text;
|
||||
}
|
||||
};
|
||||
|
||||
const setNewReqRisk = (risk) => {
|
||||
if (latestRequirement !== undefined) {
|
||||
latestRequirement.risk = risk;
|
||||
}
|
||||
};
|
||||
|
||||
const setNewReqVerifyMethod = (verifyMethod) => {
|
||||
if (latestRequirement !== undefined) {
|
||||
latestRequirement.verifyMethod = verifyMethod;
|
||||
}
|
||||
};
|
||||
|
||||
const addElement = (name) => {
|
||||
if (!elements.has(name)) {
|
||||
elements.set(name, {
|
||||
name,
|
||||
type: latestElement.type,
|
||||
docRef: latestElement.docRef,
|
||||
});
|
||||
log.info('Added new requirement: ', name);
|
||||
}
|
||||
latestElement = {};
|
||||
|
||||
return elements.get(name);
|
||||
};
|
||||
|
||||
const getElements = () => elements;
|
||||
|
||||
const setNewElementType = (type) => {
|
||||
if (latestElement !== undefined) {
|
||||
latestElement.type = type;
|
||||
}
|
||||
};
|
||||
|
||||
const setNewElementDocRef = (docRef) => {
|
||||
if (latestElement !== undefined) {
|
||||
latestElement.docRef = docRef;
|
||||
}
|
||||
};
|
||||
|
||||
const addRelationship = (type, src, dst) => {
|
||||
relations.push({
|
||||
type,
|
||||
src,
|
||||
dst,
|
||||
});
|
||||
};
|
||||
|
||||
const getRelationships = () => relations;
|
||||
|
||||
const clear = () => {
|
||||
relations = [];
|
||||
latestRequirement = {};
|
||||
requirements = new Map();
|
||||
latestElement = {};
|
||||
elements = new Map();
|
||||
commonClear();
|
||||
};
|
||||
|
||||
export default {
|
||||
RequirementType,
|
||||
RiskLevel,
|
||||
VerifyType,
|
||||
Relationships,
|
||||
|
||||
getConfig: () => getConfig().req,
|
||||
|
||||
addRequirement,
|
||||
getRequirements,
|
||||
setNewReqId,
|
||||
setNewReqText,
|
||||
setNewReqRisk,
|
||||
setNewReqVerifyMethod,
|
||||
setAccTitle,
|
||||
getAccTitle,
|
||||
setAccDescription,
|
||||
getAccDescription,
|
||||
|
||||
addElement,
|
||||
getElements,
|
||||
setNewElementType,
|
||||
setNewElementDocRef,
|
||||
|
||||
addRelationship,
|
||||
getRelationships,
|
||||
|
||||
clear,
|
||||
};
|
@@ -0,0 +1,96 @@
|
||||
import { RequirementDB } from './requirementDb.js';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import type { Relation, RelationshipType } from './types.js';
|
||||
|
||||
describe('requirementDb', () => {
|
||||
const requirementDb = new RequirementDB();
|
||||
beforeEach(() => {
|
||||
requirementDb.clear();
|
||||
});
|
||||
|
||||
it('should add a requirement', () => {
|
||||
requirementDb.addRequirement('requirement', 'Requirement');
|
||||
const requirements = requirementDb.getRequirements();
|
||||
expect(requirements.has('requirement')).toBe(true);
|
||||
});
|
||||
|
||||
it('should add an element', () => {
|
||||
requirementDb.addElement('element');
|
||||
const elements = requirementDb.getElements();
|
||||
expect(elements.has('element')).toBe(true);
|
||||
});
|
||||
|
||||
it('should add a relationship', () => {
|
||||
requirementDb.addRelationship('contains' as RelationshipType, 'src', 'dst');
|
||||
const relationships = requirementDb.getRelationships();
|
||||
const relationship = relationships.find(
|
||||
(r: Relation) => r.type === 'contains' && r.src === 'src' && r.dst === 'dst'
|
||||
);
|
||||
expect(relationship).toBeDefined();
|
||||
});
|
||||
|
||||
it('should detect single class', () => {
|
||||
requirementDb.defineClass(['a'], ['stroke-width: 8px']);
|
||||
const classes = requirementDb.getClasses();
|
||||
|
||||
expect(classes.has('a')).toBe(true);
|
||||
expect(classes.get('a')?.styles).toEqual(['stroke-width: 8px']);
|
||||
});
|
||||
|
||||
it('should detect many classes', () => {
|
||||
requirementDb.defineClass(['a', 'b'], ['stroke-width: 8px']);
|
||||
const classes = requirementDb.getClasses();
|
||||
|
||||
expect(classes.has('a')).toBe(true);
|
||||
expect(classes.has('b')).toBe(true);
|
||||
expect(classes.get('a')?.styles).toEqual(['stroke-width: 8px']);
|
||||
expect(classes.get('b')?.styles).toEqual(['stroke-width: 8px']);
|
||||
});
|
||||
|
||||
it('should detect direction', () => {
|
||||
requirementDb.setDirection('TB');
|
||||
const direction = requirementDb.getDirection();
|
||||
|
||||
expect(direction).toBe('TB');
|
||||
});
|
||||
|
||||
it('should add styles to a requirement and element', () => {
|
||||
requirementDb.addRequirement('requirement', 'Requirement');
|
||||
requirementDb.setCssStyle(['requirement'], ['color:red']);
|
||||
requirementDb.addElement('element');
|
||||
requirementDb.setCssStyle(['element'], ['stroke-width:4px', 'stroke: yellow']);
|
||||
|
||||
const requirement = requirementDb.getRequirements().get('requirement');
|
||||
const element = requirementDb.getElements().get('element');
|
||||
|
||||
expect(requirement?.cssStyles).toEqual(['color:red']);
|
||||
expect(element?.cssStyles).toEqual(['stroke-width:4px', 'stroke: yellow']);
|
||||
});
|
||||
|
||||
it('should add classes to a requirement and element', () => {
|
||||
requirementDb.addRequirement('requirement', 'Requirement');
|
||||
requirementDb.addElement('element');
|
||||
requirementDb.setClass(['requirement', 'element'], ['myClass']);
|
||||
|
||||
const requirement = requirementDb.getRequirements().get('requirement');
|
||||
const element = requirementDb.getElements().get('element');
|
||||
|
||||
expect(requirement?.classes).toEqual(['default', 'myClass']);
|
||||
expect(element?.classes).toEqual(['default', 'myClass']);
|
||||
});
|
||||
|
||||
it('should add styles to a requirement and element inherited from a class', () => {
|
||||
requirementDb.addRequirement('requirement', 'Requirement');
|
||||
requirementDb.addElement('element');
|
||||
requirementDb.defineClass(['myClass'], ['color:red']);
|
||||
requirementDb.defineClass(['myClass2'], ['stroke-width:4px', 'stroke: yellow']);
|
||||
requirementDb.setClass(['requirement'], ['myClass']);
|
||||
requirementDb.setClass(['element'], ['myClass2']);
|
||||
|
||||
const requirement = requirementDb.getRequirements().get('requirement');
|
||||
const element = requirementDb.getElements().get('element');
|
||||
|
||||
expect(requirement?.cssStyles).toEqual(['color:red']);
|
||||
expect(element?.cssStyles).toEqual(['stroke-width:4px', 'stroke: yellow']);
|
||||
});
|
||||
});
|
349
packages/mermaid/src/diagrams/requirement/requirementDb.ts
Normal file
349
packages/mermaid/src/diagrams/requirement/requirementDb.ts
Normal file
@@ -0,0 +1,349 @@
|
||||
import { getConfig } from '../../diagram-api/diagramAPI.js';
|
||||
import type { DiagramDB } from '../../diagram-api/types.js';
|
||||
import { log } from '../../logger.js';
|
||||
import type { Node, Edge } from '../../rendering-util/types.js';
|
||||
|
||||
import {
|
||||
setAccTitle,
|
||||
getAccTitle,
|
||||
getAccDescription,
|
||||
setAccDescription,
|
||||
clear as commonClear,
|
||||
setDiagramTitle,
|
||||
getDiagramTitle,
|
||||
} from '../common/commonDb.js';
|
||||
import type {
|
||||
Element,
|
||||
Relation,
|
||||
RelationshipType,
|
||||
Requirement,
|
||||
RequirementClass,
|
||||
RequirementType,
|
||||
RiskLevel,
|
||||
VerifyType,
|
||||
} from './types.js';
|
||||
|
||||
export class RequirementDB implements DiagramDB {
|
||||
private relations: Relation[] = [];
|
||||
private latestRequirement: Requirement = this.getInitialRequirement();
|
||||
private requirements = new Map<string, Requirement>();
|
||||
private latestElement: Element = this.getInitialElement();
|
||||
private elements = new Map<string, Element>();
|
||||
private classes = new Map<string, RequirementClass>();
|
||||
private direction = 'TB';
|
||||
|
||||
private RequirementType = {
|
||||
REQUIREMENT: 'Requirement',
|
||||
FUNCTIONAL_REQUIREMENT: 'Functional Requirement',
|
||||
INTERFACE_REQUIREMENT: 'Interface Requirement',
|
||||
PERFORMANCE_REQUIREMENT: 'Performance Requirement',
|
||||
PHYSICAL_REQUIREMENT: 'Physical Requirement',
|
||||
DESIGN_CONSTRAINT: 'Design Constraint',
|
||||
};
|
||||
|
||||
private RiskLevel = {
|
||||
LOW_RISK: 'Low',
|
||||
MED_RISK: 'Medium',
|
||||
HIGH_RISK: 'High',
|
||||
};
|
||||
|
||||
private VerifyType = {
|
||||
VERIFY_ANALYSIS: 'Analysis',
|
||||
VERIFY_DEMONSTRATION: 'Demonstration',
|
||||
VERIFY_INSPECTION: 'Inspection',
|
||||
VERIFY_TEST: 'Test',
|
||||
};
|
||||
|
||||
private Relationships = {
|
||||
CONTAINS: 'contains',
|
||||
COPIES: 'copies',
|
||||
DERIVES: 'derives',
|
||||
SATISFIES: 'satisfies',
|
||||
VERIFIES: 'verifies',
|
||||
REFINES: 'refines',
|
||||
TRACES: 'traces',
|
||||
};
|
||||
|
||||
constructor() {
|
||||
this.clear();
|
||||
|
||||
// Needed for JISON since it only supports direct properties
|
||||
this.setDirection = this.setDirection.bind(this);
|
||||
this.addRequirement = this.addRequirement.bind(this);
|
||||
this.setNewReqId = this.setNewReqId.bind(this);
|
||||
this.setNewReqRisk = this.setNewReqRisk.bind(this);
|
||||
this.setNewReqText = this.setNewReqText.bind(this);
|
||||
this.setNewReqVerifyMethod = this.setNewReqVerifyMethod.bind(this);
|
||||
this.addElement = this.addElement.bind(this);
|
||||
this.setNewElementType = this.setNewElementType.bind(this);
|
||||
this.setNewElementDocRef = this.setNewElementDocRef.bind(this);
|
||||
this.addRelationship = this.addRelationship.bind(this);
|
||||
this.setCssStyle = this.setCssStyle.bind(this);
|
||||
this.setClass = this.setClass.bind(this);
|
||||
this.defineClass = this.defineClass.bind(this);
|
||||
this.setAccTitle = this.setAccTitle.bind(this);
|
||||
this.setAccDescription = this.setAccDescription.bind(this);
|
||||
}
|
||||
|
||||
public getDirection() {
|
||||
return this.direction;
|
||||
}
|
||||
public setDirection(dir: string) {
|
||||
this.direction = dir;
|
||||
}
|
||||
|
||||
private resetLatestRequirement() {
|
||||
this.latestRequirement = this.getInitialRequirement();
|
||||
}
|
||||
|
||||
private resetLatestElement() {
|
||||
this.latestElement = this.getInitialElement();
|
||||
}
|
||||
|
||||
private getInitialRequirement(): Requirement {
|
||||
return {
|
||||
requirementId: '',
|
||||
text: '',
|
||||
risk: '' as RiskLevel,
|
||||
verifyMethod: '' as VerifyType,
|
||||
name: '',
|
||||
type: '' as RequirementType,
|
||||
cssStyles: [],
|
||||
classes: ['default'],
|
||||
};
|
||||
}
|
||||
|
||||
private getInitialElement(): Element {
|
||||
return {
|
||||
name: '',
|
||||
type: '',
|
||||
docRef: '',
|
||||
cssStyles: [],
|
||||
classes: ['default'],
|
||||
};
|
||||
}
|
||||
|
||||
public addRequirement(name: string, type: RequirementType) {
|
||||
if (!this.requirements.has(name)) {
|
||||
this.requirements.set(name, {
|
||||
name,
|
||||
type,
|
||||
requirementId: this.latestRequirement.requirementId,
|
||||
text: this.latestRequirement.text,
|
||||
risk: this.latestRequirement.risk,
|
||||
verifyMethod: this.latestRequirement.verifyMethod,
|
||||
cssStyles: [],
|
||||
classes: ['default'],
|
||||
});
|
||||
}
|
||||
this.resetLatestRequirement();
|
||||
|
||||
return this.requirements.get(name);
|
||||
}
|
||||
|
||||
public getRequirements() {
|
||||
return this.requirements;
|
||||
}
|
||||
|
||||
public setNewReqId(id: string) {
|
||||
if (this.latestRequirement !== undefined) {
|
||||
this.latestRequirement.requirementId = id;
|
||||
}
|
||||
}
|
||||
|
||||
public setNewReqText(text: string) {
|
||||
if (this.latestRequirement !== undefined) {
|
||||
this.latestRequirement.text = text;
|
||||
}
|
||||
}
|
||||
|
||||
public setNewReqRisk(risk: RiskLevel) {
|
||||
if (this.latestRequirement !== undefined) {
|
||||
this.latestRequirement.risk = risk;
|
||||
}
|
||||
}
|
||||
|
||||
public setNewReqVerifyMethod(verifyMethod: VerifyType) {
|
||||
if (this.latestRequirement !== undefined) {
|
||||
this.latestRequirement.verifyMethod = verifyMethod;
|
||||
}
|
||||
}
|
||||
|
||||
public addElement(name: string) {
|
||||
if (!this.elements.has(name)) {
|
||||
this.elements.set(name, {
|
||||
name,
|
||||
type: this.latestElement.type,
|
||||
docRef: this.latestElement.docRef,
|
||||
cssStyles: [],
|
||||
classes: ['default'],
|
||||
});
|
||||
log.info('Added new element: ', name);
|
||||
}
|
||||
this.resetLatestElement();
|
||||
|
||||
return this.elements.get(name);
|
||||
}
|
||||
|
||||
public getElements() {
|
||||
return this.elements;
|
||||
}
|
||||
|
||||
public setNewElementType(type: string) {
|
||||
if (this.latestElement !== undefined) {
|
||||
this.latestElement.type = type;
|
||||
}
|
||||
}
|
||||
|
||||
public setNewElementDocRef(docRef: string) {
|
||||
if (this.latestElement !== undefined) {
|
||||
this.latestElement.docRef = docRef;
|
||||
}
|
||||
}
|
||||
|
||||
public addRelationship(type: RelationshipType, src: string, dst: string) {
|
||||
this.relations.push({
|
||||
type,
|
||||
src,
|
||||
dst,
|
||||
});
|
||||
}
|
||||
|
||||
public getRelationships() {
|
||||
return this.relations;
|
||||
}
|
||||
|
||||
public clear() {
|
||||
this.relations = [];
|
||||
this.resetLatestRequirement();
|
||||
this.requirements = new Map();
|
||||
this.resetLatestElement();
|
||||
this.elements = new Map();
|
||||
this.classes = new Map();
|
||||
commonClear();
|
||||
}
|
||||
|
||||
public setCssStyle(ids: string[], styles: string[]) {
|
||||
for (const id of ids) {
|
||||
const node = this.requirements.get(id) ?? this.elements.get(id);
|
||||
if (!styles || !node) {
|
||||
return;
|
||||
}
|
||||
for (const s of styles) {
|
||||
if (s.includes(',')) {
|
||||
node.cssStyles.push(...s.split(','));
|
||||
} else {
|
||||
node.cssStyles.push(s);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public setClass(ids: string[], classNames: string[]) {
|
||||
for (const id of ids) {
|
||||
const node = this.requirements.get(id) ?? this.elements.get(id);
|
||||
if (node) {
|
||||
for (const _class of classNames) {
|
||||
node.classes.push(_class);
|
||||
const styles = this.classes.get(_class)?.styles;
|
||||
if (styles) {
|
||||
node.cssStyles.push(...styles);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public defineClass(ids: string[], style: string[]) {
|
||||
for (const id of ids) {
|
||||
let styleClass = this.classes.get(id);
|
||||
if (styleClass === undefined) {
|
||||
styleClass = { id, styles: [], textStyles: [] };
|
||||
this.classes.set(id, styleClass);
|
||||
}
|
||||
|
||||
if (style) {
|
||||
style.forEach(function (s) {
|
||||
if (/color/.exec(s)) {
|
||||
const newStyle = s.replace('fill', 'bgFill'); // .replace('color', 'fill');
|
||||
styleClass.textStyles.push(newStyle);
|
||||
}
|
||||
styleClass.styles.push(s);
|
||||
});
|
||||
}
|
||||
|
||||
this.requirements.forEach((value) => {
|
||||
if (value.classes.includes(id)) {
|
||||
value.cssStyles.push(...style.flatMap((s) => s.split(',')));
|
||||
}
|
||||
});
|
||||
this.elements.forEach((value) => {
|
||||
if (value.classes.includes(id)) {
|
||||
value.cssStyles.push(...style.flatMap((s) => s.split(',')));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public getClasses() {
|
||||
return this.classes;
|
||||
}
|
||||
|
||||
public getData() {
|
||||
const config = getConfig();
|
||||
const nodes: Node[] = [];
|
||||
const edges: Edge[] = [];
|
||||
for (const requirement of this.requirements.values()) {
|
||||
const node = requirement as unknown as Node;
|
||||
node.id = requirement.name;
|
||||
node.cssStyles = requirement.cssStyles;
|
||||
node.cssClasses = requirement.classes.join(' ');
|
||||
node.shape = 'requirementBox';
|
||||
node.look = config.look;
|
||||
nodes.push(node);
|
||||
}
|
||||
|
||||
for (const element of this.elements.values()) {
|
||||
const node = element as unknown as Node;
|
||||
node.shape = 'requirementBox';
|
||||
node.look = config.look;
|
||||
node.id = element.name;
|
||||
node.cssStyles = element.cssStyles;
|
||||
node.cssClasses = element.classes.join(' ');
|
||||
|
||||
nodes.push(node);
|
||||
}
|
||||
|
||||
for (const relation of this.relations) {
|
||||
let counter = 0;
|
||||
const isContains = relation.type === this.Relationships.CONTAINS;
|
||||
const edge: Edge = {
|
||||
id: `${relation.src}-${relation.dst}-${counter}`,
|
||||
start: this.requirements.get(relation.src)?.name ?? this.elements.get(relation.src)?.name,
|
||||
end: this.requirements.get(relation.dst)?.name ?? this.elements.get(relation.dst)?.name,
|
||||
label: `<<${relation.type}>>`,
|
||||
classes: 'relationshipLine',
|
||||
style: ['fill:none', isContains ? '' : 'stroke-dasharray: 10,7'],
|
||||
labelpos: 'c',
|
||||
thickness: 'normal',
|
||||
type: 'normal',
|
||||
pattern: isContains ? 'normal' : 'dashed',
|
||||
arrowTypeEnd: isContains ? 'requirement_contains' : 'requirement_arrow',
|
||||
look: config.look,
|
||||
};
|
||||
|
||||
edges.push(edge);
|
||||
counter++;
|
||||
}
|
||||
|
||||
return { nodes, edges, other: {}, config, direction: this.getDirection() };
|
||||
}
|
||||
|
||||
public setAccTitle = setAccTitle;
|
||||
public getAccTitle = getAccTitle;
|
||||
public setAccDescription = setAccDescription;
|
||||
public getAccDescription = getAccDescription;
|
||||
public setDiagramTitle = setDiagramTitle;
|
||||
public getDiagramTitle = getDiagramTitle;
|
||||
public getConfig = () => getConfig().requirement;
|
||||
}
|
@@ -1,13 +1,15 @@
|
||||
import type { DiagramDefinition } from '../../diagram-api/types.js';
|
||||
// @ts-ignore: JISON doesn't support types
|
||||
import parser from './parser/requirementDiagram.jison';
|
||||
import db from './requirementDb.js';
|
||||
import { RequirementDB } from './requirementDb.js';
|
||||
import styles from './styles.js';
|
||||
import renderer from './requirementRenderer.js';
|
||||
import * as renderer from './requirementRenderer.js';
|
||||
|
||||
export const diagram: DiagramDefinition = {
|
||||
parser,
|
||||
db,
|
||||
get db() {
|
||||
return new RequirementDB();
|
||||
},
|
||||
renderer,
|
||||
styles,
|
||||
};
|
||||
|
@@ -1,69 +0,0 @@
|
||||
const ReqMarkers = {
|
||||
CONTAINS: 'contains',
|
||||
ARROW: 'arrow',
|
||||
};
|
||||
|
||||
const insertLineEndings = (parentNode, conf) => {
|
||||
let containsNode = parentNode
|
||||
.append('defs')
|
||||
.append('marker')
|
||||
.attr('id', ReqMarkers.CONTAINS + '_line_ending')
|
||||
.attr('refX', 0)
|
||||
.attr('refY', conf.line_height / 2)
|
||||
.attr('markerWidth', conf.line_height)
|
||||
.attr('markerHeight', conf.line_height)
|
||||
.attr('orient', 'auto')
|
||||
.append('g');
|
||||
|
||||
containsNode
|
||||
.append('circle')
|
||||
.attr('cx', conf.line_height / 2)
|
||||
.attr('cy', conf.line_height / 2)
|
||||
.attr('r', conf.line_height / 2)
|
||||
// .attr('stroke', conf.rect_border_color)
|
||||
// .attr('stroke-width', 1)
|
||||
.attr('fill', 'none');
|
||||
|
||||
containsNode
|
||||
.append('line')
|
||||
.attr('x1', 0)
|
||||
.attr('x2', conf.line_height)
|
||||
.attr('y1', conf.line_height / 2)
|
||||
.attr('y2', conf.line_height / 2)
|
||||
// .attr('stroke', conf.rect_border_color)
|
||||
.attr('stroke-width', 1);
|
||||
|
||||
containsNode
|
||||
.append('line')
|
||||
.attr('y1', 0)
|
||||
.attr('y2', conf.line_height)
|
||||
.attr('x1', conf.line_height / 2)
|
||||
.attr('x2', conf.line_height / 2)
|
||||
// .attr('stroke', conf.rect_border_color)
|
||||
.attr('stroke-width', 1);
|
||||
|
||||
parentNode
|
||||
.append('defs')
|
||||
.append('marker')
|
||||
.attr('id', ReqMarkers.ARROW + '_line_ending')
|
||||
.attr('refX', conf.line_height)
|
||||
.attr('refY', 0.5 * conf.line_height)
|
||||
.attr('markerWidth', conf.line_height)
|
||||
.attr('markerHeight', conf.line_height)
|
||||
.attr('orient', 'auto')
|
||||
.append('path')
|
||||
.attr(
|
||||
'd',
|
||||
`M0,0
|
||||
L${conf.line_height},${conf.line_height / 2}
|
||||
M${conf.line_height},${conf.line_height / 2}
|
||||
L0,${conf.line_height}`
|
||||
)
|
||||
.attr('stroke-width', 1);
|
||||
// .attr('stroke', conf.rect_border_color);
|
||||
};
|
||||
|
||||
export default {
|
||||
ReqMarkers,
|
||||
insertLineEndings,
|
||||
};
|
@@ -1,377 +0,0 @@
|
||||
import { line, select } from 'd3';
|
||||
import { layout as dagreLayout } from 'dagre-d3-es/src/dagre/index.js';
|
||||
import * as graphlib from 'dagre-d3-es/src/graphlib/index.js';
|
||||
import { getConfig } from '../../diagram-api/diagramAPI.js';
|
||||
import { log } from '../../logger.js';
|
||||
import { configureSvgSize } from '../../setupGraphViewbox.js';
|
||||
import common from '../common/common.js';
|
||||
import markers from './requirementMarkers.js';
|
||||
|
||||
let conf = {};
|
||||
let relCnt = 0;
|
||||
|
||||
const newRectNode = (parentNode, id) => {
|
||||
return parentNode
|
||||
.insert('rect', '#' + id)
|
||||
.attr('class', 'req reqBox')
|
||||
.attr('x', 0)
|
||||
.attr('y', 0)
|
||||
.attr('width', conf.rect_min_width + 'px')
|
||||
.attr('height', conf.rect_min_height + 'px');
|
||||
};
|
||||
|
||||
const newTitleNode = (parentNode, id, txts) => {
|
||||
let x = conf.rect_min_width / 2;
|
||||
|
||||
let title = parentNode
|
||||
.append('text')
|
||||
.attr('class', 'req reqLabel reqTitle')
|
||||
.attr('id', id)
|
||||
.attr('x', x)
|
||||
.attr('y', conf.rect_padding)
|
||||
.attr('dominant-baseline', 'hanging');
|
||||
// .attr(
|
||||
// 'style',
|
||||
// 'font-family: ' + configApi.getConfig().fontFamily + '; font-size: ' + conf.fontSize + 'px'
|
||||
// )
|
||||
let i = 0;
|
||||
txts.forEach((textStr) => {
|
||||
if (i == 0) {
|
||||
title
|
||||
.append('tspan')
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('x', conf.rect_min_width / 2)
|
||||
.attr('dy', 0)
|
||||
.text(textStr);
|
||||
} else {
|
||||
title
|
||||
.append('tspan')
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('x', conf.rect_min_width / 2)
|
||||
.attr('dy', conf.line_height * 0.75)
|
||||
.text(textStr);
|
||||
}
|
||||
i++;
|
||||
});
|
||||
|
||||
let yPadding = 1.5 * conf.rect_padding;
|
||||
let linePadding = i * conf.line_height * 0.75;
|
||||
let totalY = yPadding + linePadding;
|
||||
|
||||
parentNode
|
||||
.append('line')
|
||||
.attr('class', 'req-title-line')
|
||||
.attr('x1', '0')
|
||||
.attr('x2', conf.rect_min_width)
|
||||
.attr('y1', totalY)
|
||||
.attr('y2', totalY);
|
||||
|
||||
return {
|
||||
titleNode: title,
|
||||
y: totalY,
|
||||
};
|
||||
};
|
||||
|
||||
const newBodyNode = (parentNode, id, txts, yStart) => {
|
||||
let body = parentNode
|
||||
.append('text')
|
||||
.attr('class', 'req reqLabel')
|
||||
.attr('id', id)
|
||||
.attr('x', conf.rect_padding)
|
||||
.attr('y', yStart)
|
||||
.attr('dominant-baseline', 'hanging');
|
||||
// .attr(
|
||||
// 'style',
|
||||
// 'font-family: ' + configApi.getConfig().fontFamily + '; font-size: ' + conf.fontSize + 'px'
|
||||
// );
|
||||
|
||||
let currentRow = 0;
|
||||
const charLimit = 30;
|
||||
let wrappedTxts = [];
|
||||
txts.forEach((textStr) => {
|
||||
let currentTextLen = textStr.length;
|
||||
while (currentTextLen > charLimit && currentRow < 3) {
|
||||
let firstPart = textStr.substring(0, charLimit);
|
||||
textStr = textStr.substring(charLimit, textStr.length);
|
||||
currentTextLen = textStr.length;
|
||||
wrappedTxts[wrappedTxts.length] = firstPart;
|
||||
currentRow++;
|
||||
}
|
||||
if (currentRow == 3) {
|
||||
let lastStr = wrappedTxts[wrappedTxts.length - 1];
|
||||
wrappedTxts[wrappedTxts.length - 1] = lastStr.substring(0, lastStr.length - 4) + '...';
|
||||
} else {
|
||||
wrappedTxts[wrappedTxts.length] = textStr;
|
||||
}
|
||||
currentRow = 0;
|
||||
});
|
||||
|
||||
wrappedTxts.forEach((textStr) => {
|
||||
body.append('tspan').attr('x', conf.rect_padding).attr('dy', conf.line_height).text(textStr);
|
||||
});
|
||||
|
||||
return body;
|
||||
};
|
||||
|
||||
const addEdgeLabel = (parentNode, svgPath, conf, txt) => {
|
||||
// Find the half-way point
|
||||
const len = svgPath.node().getTotalLength();
|
||||
const labelPoint = svgPath.node().getPointAtLength(len * 0.5);
|
||||
|
||||
// Append a text node containing the label
|
||||
const labelId = 'rel' + relCnt;
|
||||
relCnt++;
|
||||
|
||||
const labelNode = parentNode
|
||||
.append('text')
|
||||
.attr('class', 'req relationshipLabel')
|
||||
.attr('id', labelId)
|
||||
.attr('x', labelPoint.x)
|
||||
.attr('y', labelPoint.y)
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('dominant-baseline', 'middle')
|
||||
// .attr('style', 'font-family: ' + conf.fontFamily + '; font-size: ' + conf.fontSize + 'px')
|
||||
.text(txt);
|
||||
|
||||
// Figure out how big the opaque 'container' rectangle needs to be
|
||||
const labelBBox = labelNode.node().getBBox();
|
||||
|
||||
// Insert the opaque rectangle before the text label
|
||||
parentNode
|
||||
.insert('rect', '#' + labelId)
|
||||
.attr('class', 'req reqLabelBox')
|
||||
.attr('x', labelPoint.x - labelBBox.width / 2)
|
||||
.attr('y', labelPoint.y - labelBBox.height / 2)
|
||||
.attr('width', labelBBox.width)
|
||||
.attr('height', labelBBox.height)
|
||||
.attr('fill', 'white')
|
||||
.attr('fill-opacity', '85%');
|
||||
};
|
||||
|
||||
const drawRelationshipFromLayout = function (svg, rel, g, insert, diagObj) {
|
||||
// Find the edge relating to this relationship
|
||||
const edge = g.edge(elementString(rel.src), elementString(rel.dst));
|
||||
|
||||
// Get a function that will generate the line path
|
||||
const lineFunction = line()
|
||||
.x(function (d) {
|
||||
return d.x;
|
||||
})
|
||||
.y(function (d) {
|
||||
return d.y;
|
||||
});
|
||||
|
||||
// Insert the line at the right place
|
||||
const svgPath = svg
|
||||
.insert('path', '#' + insert)
|
||||
.attr('class', 'er relationshipLine')
|
||||
.attr('d', lineFunction(edge.points))
|
||||
.attr('fill', 'none');
|
||||
|
||||
if (rel.type == diagObj.db.Relationships.CONTAINS) {
|
||||
svgPath.attr(
|
||||
'marker-start',
|
||||
'url(' + common.getUrl(conf.arrowMarkerAbsolute) + '#' + rel.type + '_line_ending' + ')'
|
||||
);
|
||||
} else {
|
||||
svgPath.attr('stroke-dasharray', '10,7');
|
||||
svgPath.attr(
|
||||
'marker-end',
|
||||
'url(' +
|
||||
common.getUrl(conf.arrowMarkerAbsolute) +
|
||||
'#' +
|
||||
markers.ReqMarkers.ARROW +
|
||||
'_line_ending' +
|
||||
')'
|
||||
);
|
||||
}
|
||||
|
||||
addEdgeLabel(svg, svgPath, conf, `<<${rel.type}>>`);
|
||||
|
||||
return;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Map<string, any>} reqs
|
||||
* @param graph
|
||||
* @param svgNode
|
||||
*/
|
||||
export const drawReqs = (reqs, graph, svgNode) => {
|
||||
reqs.forEach((req, reqName) => {
|
||||
reqName = elementString(reqName);
|
||||
log.info('Added new requirement: ', reqName);
|
||||
|
||||
const groupNode = svgNode.append('g').attr('id', reqName);
|
||||
const textId = 'req-' + reqName;
|
||||
const rectNode = newRectNode(groupNode, textId);
|
||||
|
||||
let nodes = [];
|
||||
|
||||
let titleNodeInfo = newTitleNode(groupNode, reqName + '_title', [
|
||||
`<<${req.type}>>`,
|
||||
`${req.name}`,
|
||||
]);
|
||||
|
||||
nodes.push(titleNodeInfo.titleNode);
|
||||
|
||||
let bodyNode = newBodyNode(
|
||||
groupNode,
|
||||
reqName + '_body',
|
||||
[
|
||||
`Id: ${req.id}`,
|
||||
`Text: ${req.text}`,
|
||||
`Risk: ${req.risk}`,
|
||||
`Verification: ${req.verifyMethod}`,
|
||||
],
|
||||
titleNodeInfo.y
|
||||
);
|
||||
|
||||
nodes.push(bodyNode);
|
||||
|
||||
const rectBBox = rectNode.node().getBBox();
|
||||
|
||||
// Add the entity to the graph
|
||||
graph.setNode(reqName, {
|
||||
width: rectBBox.width,
|
||||
height: rectBBox.height,
|
||||
shape: 'rect',
|
||||
id: reqName,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Map<string, any>} els
|
||||
* @param graph
|
||||
* @param svgNode
|
||||
*/
|
||||
export const drawElements = (els, graph, svgNode) => {
|
||||
els.forEach((el, elName) => {
|
||||
const id = elementString(elName);
|
||||
|
||||
const groupNode = svgNode.append('g').attr('id', id);
|
||||
const textId = 'element-' + id;
|
||||
const rectNode = newRectNode(groupNode, textId);
|
||||
|
||||
let nodes = [];
|
||||
|
||||
let titleNodeInfo = newTitleNode(groupNode, textId + '_title', [`<<Element>>`, `${elName}`]);
|
||||
|
||||
nodes.push(titleNodeInfo.titleNode);
|
||||
|
||||
let bodyNode = newBodyNode(
|
||||
groupNode,
|
||||
textId + '_body',
|
||||
[`Type: ${el.type || 'Not Specified'}`, `Doc Ref: ${el.docRef || 'None'}`],
|
||||
titleNodeInfo.y
|
||||
);
|
||||
|
||||
nodes.push(bodyNode);
|
||||
|
||||
const rectBBox = rectNode.node().getBBox();
|
||||
|
||||
// Add the entity to the graph
|
||||
graph.setNode(id, {
|
||||
width: rectBBox.width,
|
||||
height: rectBBox.height,
|
||||
shape: 'rect',
|
||||
id: id,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const addRelationships = (relationships, g) => {
|
||||
relationships.forEach(function (r) {
|
||||
let src = elementString(r.src);
|
||||
let dst = elementString(r.dst);
|
||||
g.setEdge(src, dst, { relationship: r });
|
||||
});
|
||||
return relationships;
|
||||
};
|
||||
|
||||
const adjustEntities = function (svgNode, graph) {
|
||||
graph.nodes().forEach(function (v) {
|
||||
if (v !== undefined && graph.node(v) !== undefined) {
|
||||
svgNode.select('#' + v);
|
||||
svgNode
|
||||
.select('#' + v)
|
||||
.attr(
|
||||
'transform',
|
||||
'translate(' +
|
||||
(graph.node(v).x - graph.node(v).width / 2) +
|
||||
',' +
|
||||
(graph.node(v).y - graph.node(v).height / 2) +
|
||||
' )'
|
||||
);
|
||||
}
|
||||
});
|
||||
return;
|
||||
};
|
||||
|
||||
const elementString = (str) => {
|
||||
return str.replace(/\s/g, '').replace(/\./g, '_');
|
||||
};
|
||||
|
||||
export const draw = (text, id, _version, diagObj) => {
|
||||
conf = getConfig().requirement;
|
||||
|
||||
const securityLevel = conf.securityLevel;
|
||||
// Handle root and Document for when rendering in sandbox mode
|
||||
let sandboxElement;
|
||||
if (securityLevel === 'sandbox') {
|
||||
sandboxElement = select('#i' + id);
|
||||
}
|
||||
const root =
|
||||
securityLevel === 'sandbox'
|
||||
? select(sandboxElement.nodes()[0].contentDocument.body)
|
||||
: select('body');
|
||||
|
||||
const svg = root.select(`[id='${id}']`);
|
||||
markers.insertLineEndings(svg, conf);
|
||||
|
||||
const g = new graphlib.Graph({
|
||||
multigraph: false,
|
||||
compound: false,
|
||||
directed: true,
|
||||
})
|
||||
.setGraph({
|
||||
rankdir: conf.layoutDirection,
|
||||
marginx: 20,
|
||||
marginy: 20,
|
||||
nodesep: 100,
|
||||
edgesep: 100,
|
||||
ranksep: 100,
|
||||
})
|
||||
.setDefaultEdgeLabel(function () {
|
||||
return {};
|
||||
});
|
||||
|
||||
let requirements = diagObj.db.getRequirements();
|
||||
let elements = diagObj.db.getElements();
|
||||
let relationships = diagObj.db.getRelationships();
|
||||
|
||||
drawReqs(requirements, g, svg);
|
||||
drawElements(elements, g, svg);
|
||||
addRelationships(relationships, g);
|
||||
dagreLayout(g);
|
||||
adjustEntities(svg, g);
|
||||
|
||||
relationships.forEach(function (rel) {
|
||||
drawRelationshipFromLayout(svg, rel, g, id, diagObj);
|
||||
});
|
||||
|
||||
const padding = conf.rect_padding;
|
||||
const svgBounds = svg.node().getBBox();
|
||||
const width = svgBounds.width + padding * 2;
|
||||
const height = svgBounds.height + padding * 2;
|
||||
|
||||
configureSvgSize(svg, height, width, conf.useMaxWidth);
|
||||
|
||||
svg.attr('viewBox', `${svgBounds.x - padding} ${svgBounds.y - padding} ${width} ${height}`);
|
||||
};
|
||||
|
||||
// cspell:ignore txts
|
||||
|
||||
export default {
|
||||
draw,
|
||||
};
|
@@ -0,0 +1,36 @@
|
||||
import { getConfig } from '../../diagram-api/diagramAPI.js';
|
||||
import { log } from '../../logger.js';
|
||||
import { getDiagramElement } from '../../rendering-util/insertElementsForSize.js';
|
||||
import { getRegisteredLayoutAlgorithm, render } from '../../rendering-util/render.js';
|
||||
import { setupViewPortForSVG } from '../../rendering-util/setupViewPortForSVG.js';
|
||||
import type { LayoutData } from '../../rendering-util/types.js';
|
||||
import utils from '../../utils.js';
|
||||
|
||||
export const draw = async function (text: string, id: string, _version: string, diag: any) {
|
||||
log.info('REF0:');
|
||||
log.info('Drawing requirement diagram (unified)', id);
|
||||
const { securityLevel, state: conf, layout } = getConfig();
|
||||
|
||||
const data4Layout = diag.db.getData() as LayoutData;
|
||||
|
||||
// Create the root SVG - the element is the div containing the SVG element
|
||||
const svg = getDiagramElement(id, securityLevel);
|
||||
|
||||
data4Layout.type = diag.type;
|
||||
data4Layout.layoutAlgorithm = getRegisteredLayoutAlgorithm(layout);
|
||||
|
||||
data4Layout.nodeSpacing = conf?.nodeSpacing ?? 50;
|
||||
data4Layout.rankSpacing = conf?.rankSpacing ?? 50;
|
||||
data4Layout.markers = ['requirement_contains', 'requirement_arrow'];
|
||||
data4Layout.diagramId = id;
|
||||
await render(data4Layout, svg);
|
||||
const padding = 8;
|
||||
utils.insertTitle(
|
||||
svg,
|
||||
'requirementDiagramTitleText',
|
||||
conf?.titleTopMargin ?? 25,
|
||||
diag.db.getDiagramTitle()
|
||||
);
|
||||
|
||||
setupViewPortForSVG(svg, padding, 'requirementDiagram', conf?.useMaxWidth ?? true);
|
||||
};
|
@@ -40,6 +40,21 @@ const getStyles = (options) => `
|
||||
.relationshipLabel {
|
||||
fill: ${options.relationLabelColor};
|
||||
}
|
||||
.divider {
|
||||
stroke: ${options.nodeBorder};
|
||||
stroke-width: 1;
|
||||
}
|
||||
.label {
|
||||
font-family: ${options.fontFamily};
|
||||
color: ${options.nodeTextColor || options.textColor};
|
||||
}
|
||||
.label text,span {
|
||||
fill: ${options.nodeTextColor || options.textColor};
|
||||
color: ${options.nodeTextColor || options.textColor};
|
||||
}
|
||||
.labelBkg {
|
||||
background-color: ${options.edgeLabelBackground};
|
||||
}
|
||||
|
||||
`;
|
||||
// fill', conf.rect_fill)
|
||||
|
51
packages/mermaid/src/diagrams/requirement/types.ts
Normal file
51
packages/mermaid/src/diagrams/requirement/types.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
export type RequirementType =
|
||||
| 'Requirement'
|
||||
| 'Functional Requirement'
|
||||
| 'Interface Requirement'
|
||||
| 'Performance Requirement'
|
||||
| 'Physical Requirement'
|
||||
| 'Design Constraint';
|
||||
|
||||
export type RiskLevel = 'Low' | 'Medium' | 'High';
|
||||
|
||||
export type VerifyType = 'Analysis' | 'Demonstration' | 'Inspection' | 'Test';
|
||||
|
||||
export interface Requirement {
|
||||
name: string;
|
||||
type: RequirementType;
|
||||
requirementId: string;
|
||||
text: string;
|
||||
risk: RiskLevel;
|
||||
verifyMethod: VerifyType;
|
||||
cssStyles: string[];
|
||||
classes: string[];
|
||||
}
|
||||
|
||||
export type RelationshipType =
|
||||
| 'contains'
|
||||
| 'copies'
|
||||
| 'derives'
|
||||
| 'satisfies'
|
||||
| 'verifies'
|
||||
| 'refines'
|
||||
| 'traces';
|
||||
|
||||
export interface Relation {
|
||||
type: RelationshipType;
|
||||
src: string;
|
||||
dst: string;
|
||||
}
|
||||
|
||||
export interface Element {
|
||||
name: string;
|
||||
type: string;
|
||||
docRef: string;
|
||||
cssStyles: string[];
|
||||
classes: string[];
|
||||
}
|
||||
|
||||
export interface RequirementClass {
|
||||
id: string;
|
||||
styles: string[];
|
||||
textStyles: string[];
|
||||
}
|
@@ -61,6 +61,26 @@ element user_defined_name {
|
||||
}
|
||||
```
|
||||
|
||||
### Markdown Formatting
|
||||
|
||||
In places where user defined text is possible (like names, requirement text, element docref, etc.), you can:
|
||||
|
||||
- Surround the text in quotes: `"example text"`
|
||||
- Use markdown formatting inside quotes: `"**bold text** and *italics*"`
|
||||
|
||||
Example:
|
||||
|
||||
```mermaid-example
|
||||
requirementDiagram
|
||||
|
||||
requirement "__test_req__" {
|
||||
id: 1
|
||||
text: "*italicized text* **bold text**"
|
||||
risk: high
|
||||
verifymethod: test
|
||||
}
|
||||
```
|
||||
|
||||
### Relationship
|
||||
|
||||
Relationships are comprised of a source node, destination node, and relationship type.
|
||||
@@ -157,4 +177,140 @@ This example uses all features of the diagram.
|
||||
test_req <- copies - test_entity2
|
||||
```
|
||||
|
||||
## Direction
|
||||
|
||||
The diagram can be rendered in different directions using the `direction` statement. Valid values are:
|
||||
|
||||
- `TB` - Top to Bottom (default)
|
||||
- `BT` - Bottom to Top
|
||||
- `LR` - Left to Right
|
||||
- `RL` - Right to Left
|
||||
|
||||
Example:
|
||||
|
||||
```mermaid-example
|
||||
requirementDiagram
|
||||
|
||||
direction LR
|
||||
|
||||
requirement test_req {
|
||||
id: 1
|
||||
text: the test text.
|
||||
risk: high
|
||||
verifymethod: test
|
||||
}
|
||||
|
||||
element test_entity {
|
||||
type: simulation
|
||||
}
|
||||
|
||||
test_entity - satisfies -> test_req
|
||||
```
|
||||
|
||||
## Styling
|
||||
|
||||
Requirements and elements can be styled using direct styling or classes. As a rule of thumb, when applying styles or classes, it accepts a list of requirement or element names and a list of class names allowing multiple assignments at a time (The only exception is the shorthand syntax `:::` which can assign multiple classes but only to one requirement or element at a time).
|
||||
|
||||
### Direct Styling
|
||||
|
||||
Use the `style` keyword to apply CSS styles directly:
|
||||
|
||||
```mermaid-example
|
||||
requirementDiagram
|
||||
|
||||
requirement test_req {
|
||||
id: 1
|
||||
text: styling example
|
||||
risk: low
|
||||
verifymethod: test
|
||||
}
|
||||
|
||||
element test_entity {
|
||||
type: simulation
|
||||
}
|
||||
|
||||
style test_req fill:#ffa,stroke:#000, color: green
|
||||
style test_entity fill:#f9f,stroke:#333, color: blue
|
||||
```
|
||||
|
||||
### Class Definitions
|
||||
|
||||
Define reusable styles using `classDef`:
|
||||
|
||||
```mermaid-example
|
||||
requirementDiagram
|
||||
|
||||
requirement test_req {
|
||||
id: 1
|
||||
text: "class styling example"
|
||||
risk: low
|
||||
verifymethod: test
|
||||
}
|
||||
|
||||
element test_entity {
|
||||
type: simulation
|
||||
}
|
||||
|
||||
classDef important fill:#f96,stroke:#333,stroke-width:4px
|
||||
classDef test fill:#ffa,stroke:#000
|
||||
```
|
||||
|
||||
### Default class
|
||||
|
||||
If a class is named default it will be applied to all nodes. Specific styles and classes should be defined afterwards to override the applied default styling.
|
||||
|
||||
```
|
||||
classDef default fill:#f9f,stroke:#333,stroke-width:4px;
|
||||
```
|
||||
|
||||
### Applying Classes
|
||||
|
||||
Classes can be applied in two ways:
|
||||
|
||||
1. Using the `class` keyword:
|
||||
|
||||
```
|
||||
class test_req,test_entity important
|
||||
```
|
||||
|
||||
2. Using the shorthand syntax with `:::` either during the definition or afterwards:
|
||||
|
||||
```
|
||||
requirement test_req:::important {
|
||||
id: 1
|
||||
text: class styling example
|
||||
risk: low
|
||||
verifymethod: test
|
||||
}
|
||||
```
|
||||
|
||||
```
|
||||
element test_elem {
|
||||
}
|
||||
|
||||
test_elem:::myClass
|
||||
```
|
||||
|
||||
### Combined Example
|
||||
|
||||
```mermaid-example
|
||||
requirementDiagram
|
||||
|
||||
requirement test_req:::important {
|
||||
id: 1
|
||||
text: "class styling example"
|
||||
risk: low
|
||||
verifymethod: test
|
||||
}
|
||||
|
||||
element test_entity {
|
||||
type: simulation
|
||||
}
|
||||
|
||||
classDef important font-weight:bold
|
||||
|
||||
class test_entity important
|
||||
style test_entity fill:#f9f,stroke:#333
|
||||
```
|
||||
|
||||
<!--- cspell:ignore reqs --->
|
||||
|
@@ -39,6 +39,8 @@ const arrowTypesMap = {
|
||||
zero_or_one: 'zeroOrOne',
|
||||
one_or_more: 'oneOrMore',
|
||||
zero_or_more: 'zeroOrMore',
|
||||
requirement_arrow: 'requirement_arrow',
|
||||
requirement_contains: 'requirement_contains',
|
||||
} as const;
|
||||
|
||||
const addEdgeMarker = (
|
||||
|
@@ -398,6 +398,42 @@ const zero_or_more = (elem, type, id) => {
|
||||
.attr('orient', 'auto');
|
||||
endMarker.append('circle').attr('fill', 'white').attr('cx', 9).attr('cy', 18).attr('r', 6);
|
||||
endMarker.append('path').attr('d', 'M21,18 Q39,0 57,18 Q39,36 21,18');
|
||||
const requirement_arrow = (elem, type, id) => {
|
||||
elem
|
||||
.append('defs')
|
||||
.append('marker')
|
||||
.attr('id', id + '_' + type + '-requirement_arrowEnd')
|
||||
.attr('refX', 20)
|
||||
.attr('refY', 10)
|
||||
.attr('markerWidth', 20)
|
||||
.attr('markerHeight', 20)
|
||||
.attr('orient', 'auto')
|
||||
.append('path')
|
||||
.attr(
|
||||
'd',
|
||||
`M0,0
|
||||
L20,10
|
||||
M20,10
|
||||
L0,20`
|
||||
);
|
||||
};
|
||||
const requirement_contains = (elem, type, id) => {
|
||||
const containsNode = elem
|
||||
.append('defs')
|
||||
.append('marker')
|
||||
.attr('id', id + '_' + type + '-requirement_containsEnd')
|
||||
.attr('refX', 20)
|
||||
.attr('refY', 10)
|
||||
.attr('markerWidth', 20)
|
||||
.attr('markerHeight', 20)
|
||||
.attr('orient', 'auto')
|
||||
.append('g');
|
||||
|
||||
containsNode.append('circle').attr('cx', 10).attr('cy', 10).attr('r', 10).attr('fill', 'none');
|
||||
|
||||
containsNode.append('line').attr('x1', 0).attr('x2', 20).attr('y1', 10).attr('y2', 10);
|
||||
|
||||
containsNode.append('line').attr('y1', 0).attr('y2', 20).attr('x1', 10).attr('x2', 10);
|
||||
};
|
||||
|
||||
// TODO rename the class diagram markers to something shape descriptive and semantic free
|
||||
@@ -415,5 +451,7 @@ const markers = {
|
||||
zero_or_one,
|
||||
one_or_more,
|
||||
zero_or_more,
|
||||
requirement_arrow,
|
||||
requirement_contains,
|
||||
};
|
||||
export default insertMarkers;
|
||||
|
@@ -59,6 +59,7 @@ import { waveRectangle } from './shapes/waveRectangle.js';
|
||||
import { windowPane } from './shapes/windowPane.js';
|
||||
import { erBox } from './shapes/erBox.js';
|
||||
import { classBox } from './shapes/classBox.js';
|
||||
import { requirementBox } from './shapes/requirementBox.js';
|
||||
import { kanbanItem } from './shapes/kanbanItem.js';
|
||||
|
||||
type ShapeHandler = <T extends SVGGraphicsElement>(
|
||||
@@ -480,6 +481,9 @@ const generateShapeMap = () => {
|
||||
|
||||
// er diagram
|
||||
erBox,
|
||||
|
||||
// Requirement diagram
|
||||
requirementBox,
|
||||
} as const;
|
||||
|
||||
const entries = [
|
||||
|
@@ -0,0 +1,222 @@
|
||||
import { getNodeClasses, updateNodeBounds } from './util.js';
|
||||
import intersect from '../intersect/index.js';
|
||||
import type { Node } from '../../types.js';
|
||||
import { styles2String, userNodeOverrides } from './handDrawnShapeStyles.js';
|
||||
import rough from 'roughjs';
|
||||
import type { D3Selection } from '../../../types.js';
|
||||
import { calculateTextWidth, decodeEntities } from '../../../utils.js';
|
||||
import { getConfig, sanitizeText } from '../../../diagram-api/diagramAPI.js';
|
||||
import { createText } from '../../createText.js';
|
||||
import { select } from 'd3';
|
||||
import type { Requirement, Element } from '../../../diagrams/requirement/types.js';
|
||||
|
||||
export async function requirementBox<T extends SVGGraphicsElement>(
|
||||
parent: D3Selection<T>,
|
||||
node: Node
|
||||
) {
|
||||
const { labelStyles, nodeStyles } = styles2String(node);
|
||||
node.labelStyle = labelStyles;
|
||||
const requirementNode = node as unknown as Requirement;
|
||||
const elementNode = node as unknown as Element;
|
||||
const padding = 20;
|
||||
const gap = 20;
|
||||
const isRequirementNode = 'verifyMethod' in node;
|
||||
const classes = getNodeClasses(node);
|
||||
|
||||
// Add outer g element
|
||||
const shapeSvg = parent
|
||||
.insert('g')
|
||||
.attr('class', classes)
|
||||
.attr('id', node.domId ?? node.id);
|
||||
|
||||
let typeHeight;
|
||||
if (isRequirementNode) {
|
||||
typeHeight = await addText(
|
||||
shapeSvg,
|
||||
`<<${requirementNode.type}>>`,
|
||||
0,
|
||||
node.labelStyle
|
||||
);
|
||||
} else {
|
||||
typeHeight = await addText(shapeSvg, '<<Element>>', 0, node.labelStyle);
|
||||
}
|
||||
|
||||
let accumulativeHeight = typeHeight;
|
||||
const nameHeight = await addText(
|
||||
shapeSvg,
|
||||
requirementNode.name,
|
||||
accumulativeHeight,
|
||||
node.labelStyle + '; font-weight: bold;'
|
||||
);
|
||||
accumulativeHeight += nameHeight + gap;
|
||||
|
||||
// Requirement
|
||||
if (isRequirementNode) {
|
||||
const idHeight = await addText(
|
||||
shapeSvg,
|
||||
`${requirementNode.requirementId ? `Id: ${requirementNode.requirementId}` : ''}`,
|
||||
accumulativeHeight,
|
||||
node.labelStyle
|
||||
);
|
||||
|
||||
accumulativeHeight += idHeight;
|
||||
const textHeight = await addText(
|
||||
shapeSvg,
|
||||
`${requirementNode.text ? `Text: ${requirementNode.text}` : ''}`,
|
||||
accumulativeHeight,
|
||||
node.labelStyle
|
||||
);
|
||||
accumulativeHeight += textHeight;
|
||||
const riskHeight = await addText(
|
||||
shapeSvg,
|
||||
`${requirementNode.risk ? `Risk: ${requirementNode.risk}` : ''}`,
|
||||
accumulativeHeight,
|
||||
node.labelStyle
|
||||
);
|
||||
accumulativeHeight += riskHeight;
|
||||
await addText(
|
||||
shapeSvg,
|
||||
`${requirementNode.verifyMethod ? `Verification: ${requirementNode.verifyMethod}` : ''}`,
|
||||
accumulativeHeight,
|
||||
node.labelStyle
|
||||
);
|
||||
} else {
|
||||
// Element
|
||||
const typeHeight = await addText(
|
||||
shapeSvg,
|
||||
`${elementNode.type ? `Type: ${elementNode.type}` : ''}`,
|
||||
accumulativeHeight,
|
||||
node.labelStyle
|
||||
);
|
||||
accumulativeHeight += typeHeight;
|
||||
await addText(
|
||||
shapeSvg,
|
||||
`${elementNode.docRef ? `Doc Ref: ${elementNode.docRef}` : ''}`,
|
||||
accumulativeHeight,
|
||||
node.labelStyle
|
||||
);
|
||||
}
|
||||
|
||||
const totalWidth = (shapeSvg.node()?.getBBox().width ?? 200) + padding;
|
||||
const totalHeight = (shapeSvg.node()?.getBBox().height ?? 200) + padding;
|
||||
const x = -totalWidth / 2;
|
||||
const y = -totalHeight / 2;
|
||||
|
||||
// Setup roughjs
|
||||
// @ts-ignore TODO: Fix rough typings
|
||||
const rc = rough.svg(shapeSvg);
|
||||
const options = userNodeOverrides(node, {});
|
||||
|
||||
if (node.look !== 'handDrawn') {
|
||||
options.roughness = 0;
|
||||
options.fillStyle = 'solid';
|
||||
}
|
||||
|
||||
// Create and center rectangle
|
||||
const roughRect = rc.rectangle(x, y, totalWidth, totalHeight, options);
|
||||
|
||||
const rect = shapeSvg.insert(() => roughRect, ':first-child');
|
||||
rect.attr('class', 'basic label-container').attr('style', nodeStyles);
|
||||
|
||||
// Re-translate labels now that rect is centered
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
shapeSvg.selectAll('.label').each((_: any, i: number, nodes: any) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const text = select<any, unknown>(nodes[i]);
|
||||
|
||||
const transform = text.attr('transform');
|
||||
let translateX = 0;
|
||||
let translateY = 0;
|
||||
if (transform) {
|
||||
const regex = RegExp(/translate\(([^,]+),([^)]+)\)/);
|
||||
const translate = regex.exec(transform);
|
||||
if (translate) {
|
||||
translateX = parseFloat(translate[1]);
|
||||
translateY = parseFloat(translate[2]);
|
||||
}
|
||||
}
|
||||
|
||||
const newTranslateY = translateY - totalHeight / 2;
|
||||
let newTranslateX = x + padding / 2;
|
||||
|
||||
// Keep type and name labels centered.
|
||||
if (i === 0 || i === 1) {
|
||||
newTranslateX = translateX;
|
||||
}
|
||||
// Set the updated transform attribute
|
||||
text.attr('transform', `translate(${newTranslateX}, ${newTranslateY + padding})`);
|
||||
});
|
||||
|
||||
// Insert divider line if there is body text
|
||||
if (accumulativeHeight > typeHeight + nameHeight + gap) {
|
||||
const roughLine = rc.line(
|
||||
x,
|
||||
y + typeHeight + nameHeight + gap,
|
||||
x + totalWidth,
|
||||
y + typeHeight + nameHeight + gap,
|
||||
options
|
||||
);
|
||||
const dividerLine = shapeSvg.insert(() => roughLine);
|
||||
dividerLine.attr('style', nodeStyles);
|
||||
}
|
||||
|
||||
updateNodeBounds(node, rect);
|
||||
|
||||
node.intersect = function (point) {
|
||||
return intersect.rect(node, point);
|
||||
};
|
||||
|
||||
return shapeSvg;
|
||||
}
|
||||
|
||||
async function addText<T extends SVGGraphicsElement>(
|
||||
parentGroup: D3Selection<T>,
|
||||
inputText: string,
|
||||
yOffset: number,
|
||||
style = ''
|
||||
) {
|
||||
if (inputText === '') {
|
||||
return 0;
|
||||
}
|
||||
const textEl = parentGroup.insert('g').attr('class', 'label').attr('style', style);
|
||||
const config = getConfig();
|
||||
const useHtmlLabels = config.htmlLabels ?? true;
|
||||
|
||||
const text = await createText(
|
||||
textEl,
|
||||
sanitizeText(decodeEntities(inputText)),
|
||||
{
|
||||
width: calculateTextWidth(inputText, config) + 50, // Add room for error when splitting text into multiple lines
|
||||
classes: 'markdown-node-label',
|
||||
useHtmlLabels,
|
||||
style,
|
||||
},
|
||||
config
|
||||
);
|
||||
let bbox;
|
||||
|
||||
if (!useHtmlLabels) {
|
||||
const textChild = text.children[0];
|
||||
for (const child of textChild.children) {
|
||||
child.textContent = child.textContent.replaceAll('>', '>').replaceAll('<', '<');
|
||||
if (style) {
|
||||
child.setAttribute('style', style);
|
||||
}
|
||||
}
|
||||
// Get the bounding box after the text update
|
||||
bbox = text.getBBox();
|
||||
// Add extra height so it is similar to the html labels
|
||||
bbox.height += 6;
|
||||
} else {
|
||||
const div = text.children[0];
|
||||
const dv = select(text);
|
||||
|
||||
bbox = div.getBoundingClientRect();
|
||||
dv.attr('width', bbox.width);
|
||||
dv.attr('height', bbox.height);
|
||||
}
|
||||
|
||||
// Center text and offset by yOffset
|
||||
textEl.attr('transform', `translate(${-bbox.width / 2},${-bbox.height / 2 + yOffset})`);
|
||||
return bbox.height;
|
||||
}
|
@@ -337,6 +337,9 @@ export const calculatePoint = (points: Point[], distanceToTraverse: number): Poi
|
||||
for (const point of points) {
|
||||
if (prevPoint) {
|
||||
const vectorDistance = distance(point, prevPoint);
|
||||
if (vectorDistance === 0) {
|
||||
return prevPoint;
|
||||
}
|
||||
if (vectorDistance < remainingDistance) {
|
||||
remainingDistance -= vectorDistance;
|
||||
} else {
|
||||
|
Reference in New Issue
Block a user