mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-08-23 10:16:43 +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
|
# 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
|
## Properties
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ Defined in: [packages/mermaid/src/utils.ts:780](https://github.com/mermaid-js/me
|
|||||||
|
|
||||||
> `optional` **error**: `any`
|
> `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`
|
> **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`
|
> `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`
|
> **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
|
### Relationship
|
||||||
|
|
||||||
Relationships are comprised of a source node, destination node, and relationship type.
|
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
|
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 --->
|
<!--- cspell:ignore reqs --->
|
||||||
|
@@ -9,6 +9,7 @@
|
|||||||
%x string
|
%x string
|
||||||
%x token
|
%x token
|
||||||
%x unqString
|
%x unqString
|
||||||
|
%x style
|
||||||
%x acc_title
|
%x acc_title
|
||||||
%x acc_descr
|
%x acc_descr
|
||||||
%x acc_descr_multiline
|
%x acc_descr_multiline
|
||||||
@@ -22,6 +23,10 @@ accDescr\s*":"\s* { this.begin("ac
|
|||||||
accDescr\s*"{"\s* { this.begin("acc_descr_multiline");}
|
accDescr\s*"{"\s* { this.begin("acc_descr_multiline");}
|
||||||
<acc_descr_multiline>[\}] { this.popState(); }
|
<acc_descr_multiline>[\}] { this.popState(); }
|
||||||
<acc_descr_multiline>[^\}]* return "acc_descr_multiline_value";
|
<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';
|
(\r?\n)+ return 'NEWLINE';
|
||||||
\s+ /* skip all whitespace */
|
\s+ /* skip all whitespace */
|
||||||
\#[^\n]* /* skip comments */
|
\#[^\n]* /* skip comments */
|
||||||
@@ -32,6 +37,7 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multili
|
|||||||
|
|
||||||
"{" return 'STRUCT_START';
|
"{" return 'STRUCT_START';
|
||||||
"}" return 'STRUCT_STOP';
|
"}" return 'STRUCT_STOP';
|
||||||
|
":"{3} return 'STYLE_SEPARATOR';
|
||||||
":" return 'COLONSEP';
|
":" return 'COLONSEP';
|
||||||
|
|
||||||
"id" return 'ID';
|
"id" return 'ID';
|
||||||
@@ -68,6 +74,20 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multili
|
|||||||
"type" return 'TYPE';
|
"type" return 'TYPE';
|
||||||
"docref" return 'DOCREF';
|
"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_L';
|
||||||
"->" {return 'END_ARROW_R';}
|
"->" {return 'END_ARROW_R';}
|
||||||
"-" {return 'LINE';}
|
"-" {return 'LINE';}
|
||||||
@@ -76,7 +96,11 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multili
|
|||||||
<string>["] { this.popState(); }
|
<string>["] { this.popState(); }
|
||||||
<string>[^"]* { return "qString"; }
|
<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
|
/lex
|
||||||
|
|
||||||
@@ -101,11 +125,28 @@ diagram
|
|||||||
| elementDef diagram
|
| elementDef diagram
|
||||||
| relationshipDef diagram
|
| relationshipDef diagram
|
||||||
| directive 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
|
requirementDef
|
||||||
: requirementType requirementName STRUCT_START NEWLINE requirementBody
|
: requirementType requirementName STRUCT_START NEWLINE requirementBody { yy.addRequirement($2, $1) }
|
||||||
{ yy.addRequirement($2, $1) };
|
| requirementType requirementName STYLE_SEPARATOR idList STRUCT_START NEWLINE requirementBody { yy.addRequirement($2, $1); yy.setClass([$2], $4); }
|
||||||
|
;
|
||||||
|
|
||||||
requirementBody
|
requirementBody
|
||||||
: ID COLONSEP id NEWLINE requirementBody
|
: ID COLONSEP id NEWLINE requirementBody
|
||||||
@@ -149,8 +190,9 @@ verifyType
|
|||||||
{ $$=yy.VerifyType.VERIFY_TEST;};
|
{ $$=yy.VerifyType.VERIFY_TEST;};
|
||||||
|
|
||||||
elementDef
|
elementDef
|
||||||
: ELEMENT elementName STRUCT_START NEWLINE elementBody
|
: ELEMENT elementName STRUCT_START NEWLINE elementBody { yy.addElement($2) }
|
||||||
{ yy.addElement($2) };
|
| ELEMENT elementName STYLE_SEPARATOR idList STRUCT_START NEWLINE elementBody { yy.addElement($2); yy.setClass([$2], $4); }
|
||||||
|
;
|
||||||
|
|
||||||
elementBody
|
elementBody
|
||||||
: TYPE COLONSEP type NEWLINE elementBody
|
: TYPE COLONSEP type NEWLINE elementBody
|
||||||
@@ -182,6 +224,38 @@ relationship
|
|||||||
| TRACES
|
| TRACES
|
||||||
{ $$=yy.Relationships.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;
|
requirementName: unqString | qString;
|
||||||
id : unqString | qString;
|
id : unqString | qString;
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { setConfig } from '../../../config.js';
|
import { setConfig } from '../../../config.js';
|
||||||
import requirementDb from '../requirementDb.js';
|
import { RequirementDB } from '../requirementDb.js';
|
||||||
import reqDiagram from './requirementDiagram.jison';
|
import reqDiagram from './requirementDiagram.jison';
|
||||||
|
|
||||||
setConfig({
|
setConfig({
|
||||||
@@ -7,6 +7,7 @@ setConfig({
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('when parsing requirement diagram it...', function () {
|
describe('when parsing requirement diagram it...', function () {
|
||||||
|
const requirementDb = new RequirementDB();
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
reqDiagram.parser.yy = requirementDb;
|
reqDiagram.parser.yy = requirementDb;
|
||||||
reqDiagram.parser.yy.clear();
|
reqDiagram.parser.yy.clear();
|
||||||
@@ -37,7 +38,7 @@ describe('when parsing requirement diagram it...', function () {
|
|||||||
|
|
||||||
let foundReq = requirementDb.getRequirements().get(expectedName);
|
let foundReq = requirementDb.getRequirements().get(expectedName);
|
||||||
expect(foundReq).toBeDefined();
|
expect(foundReq).toBeDefined();
|
||||||
expect(foundReq.id).toBe(expectedId);
|
expect(foundReq.requirementId).toBe(expectedId);
|
||||||
expect(foundReq.text).toBe(expectedText);
|
expect(foundReq.text).toBe(expectedText);
|
||||||
|
|
||||||
expect(requirementDb.getElements().size).toBe(0);
|
expect(requirementDb.getElements().size).toBe(0);
|
||||||
@@ -599,4 +600,251 @@ line 2`;
|
|||||||
expect(reqDiagram.parser.yy.getElements().size).toBe(1);
|
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';
|
import type { DiagramDefinition } from '../../diagram-api/types.js';
|
||||||
// @ts-ignore: JISON doesn't support types
|
// @ts-ignore: JISON doesn't support types
|
||||||
import parser from './parser/requirementDiagram.jison';
|
import parser from './parser/requirementDiagram.jison';
|
||||||
import db from './requirementDb.js';
|
import { RequirementDB } from './requirementDb.js';
|
||||||
import styles from './styles.js';
|
import styles from './styles.js';
|
||||||
import renderer from './requirementRenderer.js';
|
import * as renderer from './requirementRenderer.js';
|
||||||
|
|
||||||
export const diagram: DiagramDefinition = {
|
export const diagram: DiagramDefinition = {
|
||||||
parser,
|
parser,
|
||||||
db,
|
get db() {
|
||||||
|
return new RequirementDB();
|
||||||
|
},
|
||||||
renderer,
|
renderer,
|
||||||
styles,
|
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 {
|
.relationshipLabel {
|
||||||
fill: ${options.relationLabelColor};
|
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)
|
// 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
|
### Relationship
|
||||||
|
|
||||||
Relationships are comprised of a source node, destination node, and relationship type.
|
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
|
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 --->
|
<!--- cspell:ignore reqs --->
|
||||||
|
@@ -39,6 +39,8 @@ const arrowTypesMap = {
|
|||||||
zero_or_one: 'zeroOrOne',
|
zero_or_one: 'zeroOrOne',
|
||||||
one_or_more: 'oneOrMore',
|
one_or_more: 'oneOrMore',
|
||||||
zero_or_more: 'zeroOrMore',
|
zero_or_more: 'zeroOrMore',
|
||||||
|
requirement_arrow: 'requirement_arrow',
|
||||||
|
requirement_contains: 'requirement_contains',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const addEdgeMarker = (
|
const addEdgeMarker = (
|
||||||
|
@@ -398,6 +398,42 @@ const zero_or_more = (elem, type, id) => {
|
|||||||
.attr('orient', 'auto');
|
.attr('orient', 'auto');
|
||||||
endMarker.append('circle').attr('fill', 'white').attr('cx', 9).attr('cy', 18).attr('r', 6);
|
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');
|
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
|
// TODO rename the class diagram markers to something shape descriptive and semantic free
|
||||||
@@ -415,5 +451,7 @@ const markers = {
|
|||||||
zero_or_one,
|
zero_or_one,
|
||||||
one_or_more,
|
one_or_more,
|
||||||
zero_or_more,
|
zero_or_more,
|
||||||
|
requirement_arrow,
|
||||||
|
requirement_contains,
|
||||||
};
|
};
|
||||||
export default insertMarkers;
|
export default insertMarkers;
|
||||||
|
@@ -59,6 +59,7 @@ import { waveRectangle } from './shapes/waveRectangle.js';
|
|||||||
import { windowPane } from './shapes/windowPane.js';
|
import { windowPane } from './shapes/windowPane.js';
|
||||||
import { erBox } from './shapes/erBox.js';
|
import { erBox } from './shapes/erBox.js';
|
||||||
import { classBox } from './shapes/classBox.js';
|
import { classBox } from './shapes/classBox.js';
|
||||||
|
import { requirementBox } from './shapes/requirementBox.js';
|
||||||
import { kanbanItem } from './shapes/kanbanItem.js';
|
import { kanbanItem } from './shapes/kanbanItem.js';
|
||||||
|
|
||||||
type ShapeHandler = <T extends SVGGraphicsElement>(
|
type ShapeHandler = <T extends SVGGraphicsElement>(
|
||||||
@@ -480,6 +481,9 @@ const generateShapeMap = () => {
|
|||||||
|
|
||||||
// er diagram
|
// er diagram
|
||||||
erBox,
|
erBox,
|
||||||
|
|
||||||
|
// Requirement diagram
|
||||||
|
requirementBox,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const entries = [
|
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) {
|
for (const point of points) {
|
||||||
if (prevPoint) {
|
if (prevPoint) {
|
||||||
const vectorDistance = distance(point, prevPoint);
|
const vectorDistance = distance(point, prevPoint);
|
||||||
|
if (vectorDistance === 0) {
|
||||||
|
return prevPoint;
|
||||||
|
}
|
||||||
if (vectorDistance < remainingDistance) {
|
if (vectorDistance < remainingDistance) {
|
||||||
remainingDistance -= vectorDistance;
|
remainingDistance -= vectorDistance;
|
||||||
} else {
|
} else {
|
||||||
|
Reference in New Issue
Block a user