Merge branch 'develop' into update-er-diagram

This commit is contained in:
Yari DeWalt
2025-02-26 08:23:04 -08:00
committed by GitHub
20 changed files with 2257 additions and 630 deletions

View 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
);
});
});
});

View File

@@ -10,7 +10,7 @@
# Interface: DetailedError
Defined in: [packages/mermaid/src/utils.ts:780](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/utils.ts#L780)
Defined in: [packages/mermaid/src/utils.ts:783](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/utils.ts#L783)
## Properties
@@ -18,7 +18,7 @@ Defined in: [packages/mermaid/src/utils.ts:780](https://github.com/mermaid-js/me
> `optional` **error**: `any`
Defined in: [packages/mermaid/src/utils.ts:785](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/utils.ts#L785)
Defined in: [packages/mermaid/src/utils.ts:788](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/utils.ts#L788)
---
@@ -26,7 +26,7 @@ Defined in: [packages/mermaid/src/utils.ts:785](https://github.com/mermaid-js/me
> **hash**: `any`
Defined in: [packages/mermaid/src/utils.ts:783](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/utils.ts#L783)
Defined in: [packages/mermaid/src/utils.ts:786](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/utils.ts#L786)
---
@@ -34,7 +34,7 @@ Defined in: [packages/mermaid/src/utils.ts:783](https://github.com/mermaid-js/me
> `optional` **message**: `string`
Defined in: [packages/mermaid/src/utils.ts:786](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/utils.ts#L786)
Defined in: [packages/mermaid/src/utils.ts:789](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/utils.ts#L789)
---
@@ -42,4 +42,4 @@ Defined in: [packages/mermaid/src/utils.ts:786](https://github.com/mermaid-js/me
> **str**: `string`
Defined in: [packages/mermaid/src/utils.ts:781](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/utils.ts#L781)
Defined in: [packages/mermaid/src/utils.ts:784](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/utils.ts#L784)

View File

@@ -84,6 +84,37 @@ element user_defined_name {
}
```
### Markdown Formatting
In places where user defined text is possible (like names, requirement text, element docref, etc.), you can:
- Surround the text in quotes: `"example text"`
- Use markdown formatting inside quotes: `"**bold text** and *italics*"`
Example:
```mermaid-example
requirementDiagram
requirement "__test_req__" {
id: 1
text: "*italicized text* **bold text**"
risk: high
verifymethod: test
}
```
```mermaid
requirementDiagram
requirement "__test_req__" {
id: 1
text: "*italicized text* **bold text**"
risk: high
verifymethod: test
}
```
### Relationship
Relationships are comprised of a source node, destination node, and relationship type.
@@ -250,4 +281,215 @@ This example uses all features of the diagram.
test_req <- copies - test_entity2
```
## Direction
The diagram can be rendered in different directions using the `direction` statement. Valid values are:
- `TB` - Top to Bottom (default)
- `BT` - Bottom to Top
- `LR` - Left to Right
- `RL` - Right to Left
Example:
```mermaid-example
requirementDiagram
direction LR
requirement test_req {
id: 1
text: the test text.
risk: high
verifymethod: test
}
element test_entity {
type: simulation
}
test_entity - satisfies -> test_req
```
```mermaid
requirementDiagram
direction LR
requirement test_req {
id: 1
text: the test text.
risk: high
verifymethod: test
}
element test_entity {
type: simulation
}
test_entity - satisfies -> test_req
```
## Styling
Requirements and elements can be styled using direct styling or classes. As a rule of thumb, when applying styles or classes, it accepts a list of requirement or element names and a list of class names allowing multiple assignments at a time (The only exception is the shorthand syntax `:::` which can assign multiple classes but only to one requirement or element at a time).
### Direct Styling
Use the `style` keyword to apply CSS styles directly:
```mermaid-example
requirementDiagram
requirement test_req {
id: 1
text: styling example
risk: low
verifymethod: test
}
element test_entity {
type: simulation
}
style test_req fill:#ffa,stroke:#000, color: green
style test_entity fill:#f9f,stroke:#333, color: blue
```
```mermaid
requirementDiagram
requirement test_req {
id: 1
text: styling example
risk: low
verifymethod: test
}
element test_entity {
type: simulation
}
style test_req fill:#ffa,stroke:#000, color: green
style test_entity fill:#f9f,stroke:#333, color: blue
```
### Class Definitions
Define reusable styles using `classDef`:
```mermaid-example
requirementDiagram
requirement test_req {
id: 1
text: "class styling example"
risk: low
verifymethod: test
}
element test_entity {
type: simulation
}
classDef important fill:#f96,stroke:#333,stroke-width:4px
classDef test fill:#ffa,stroke:#000
```
```mermaid
requirementDiagram
requirement test_req {
id: 1
text: "class styling example"
risk: low
verifymethod: test
}
element test_entity {
type: simulation
}
classDef important fill:#f96,stroke:#333,stroke-width:4px
classDef test fill:#ffa,stroke:#000
```
### Default class
If a class is named default it will be applied to all nodes. Specific styles and classes should be defined afterwards to override the applied default styling.
```
classDef default fill:#f9f,stroke:#333,stroke-width:4px;
```
### Applying Classes
Classes can be applied in two ways:
1. Using the `class` keyword:
```
class test_req,test_entity important
```
2. Using the shorthand syntax with `:::` either during the definition or afterwards:
```
requirement test_req:::important {
id: 1
text: class styling example
risk: low
verifymethod: test
}
```
```
element test_elem {
}
test_elem:::myClass
```
### Combined Example
```mermaid-example
requirementDiagram
requirement test_req:::important {
id: 1
text: "class styling example"
risk: low
verifymethod: test
}
element test_entity {
type: simulation
}
classDef important font-weight:bold
class test_entity important
style test_entity fill:#f9f,stroke:#333
```
```mermaid
requirementDiagram
requirement test_req:::important {
id: 1
text: "class styling example"
risk: low
verifymethod: test
}
element test_entity {
type: simulation
}
classDef important font-weight:bold
class test_entity important
style test_entity fill:#f9f,stroke:#333
```
<!--- cspell:ignore reqs --->

View File

@@ -9,6 +9,7 @@
%x string
%x token
%x unqString
%x style
%x acc_title
%x acc_descr
%x acc_descr_multiline
@@ -22,6 +23,10 @@ accDescr\s*":"\s* { this.begin("ac
accDescr\s*"{"\s* { this.begin("acc_descr_multiline");}
<acc_descr_multiline>[\}] { this.popState(); }
<acc_descr_multiline>[^\}]* return "acc_descr_multiline_value";
.*direction\s+TB[^\n]* return 'direction_tb';
.*direction\s+BT[^\n]* return 'direction_bt';
.*direction\s+RL[^\n]* return 'direction_rl';
.*direction\s+LR[^\n]* return 'direction_lr';
(\r?\n)+ return 'NEWLINE';
\s+ /* skip all whitespace */
\#[^\n]* /* skip comments */
@@ -32,6 +37,7 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multili
"{" return 'STRUCT_START';
"}" return 'STRUCT_STOP';
":"{3} return 'STYLE_SEPARATOR';
":" return 'COLONSEP';
"id" return 'ID';
@@ -68,6 +74,20 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multili
"type" return 'TYPE';
"docref" return 'DOCREF';
"style" { this.begin("style"); return 'STYLE'; }
<style>\w+ return 'ALPHA';
<style>":" return 'COLON';
<style>";" return 'SEMICOLON';
<style>"%" return 'PERCENT';
<style>"-" return 'MINUS';
<style>"#" return 'BRKT';
<style>" " /* skip spaces */
<style>["] { this.begin("string"); }
<style>\n { this.popState(); }
"classDef" { this.begin("style"); return 'CLASSDEF'; }
"class" { this.begin("style"); return 'CLASS'; }
"<-" return 'END_ARROW_L';
"->" {return 'END_ARROW_R';}
"-" {return 'LINE';}
@@ -76,7 +96,11 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multili
<string>["] { this.popState(); }
<string>[^"]* { return "qString"; }
[\w][^\r\n\{\<\>\-\=]* { yytext = yytext.trim(); return 'unqString';}
[\w][^:,\r\n\{\<\>\-\=]* { yytext = yytext.trim(); return 'unqString';}
<*>\w+ return 'ALPHA';
<*>[0-9]+ return 'NUM';
<*>"," return 'COMMA';
/lex
@@ -101,11 +125,28 @@ diagram
| elementDef diagram
| relationshipDef diagram
| directive diagram
| NEWLINE diagram;
| direction diagram
| styleStatement diagram
| classDefStatement diagram
| classStatement diagram
| NEWLINE diagram
;
direction
: direction_tb
{ yy.setDirection('TB');}
| direction_bt
{ yy.setDirection('BT');}
| direction_rl
{ yy.setDirection('RL');}
| direction_lr
{ yy.setDirection('LR');}
;
requirementDef
: requirementType requirementName STRUCT_START NEWLINE requirementBody
{ yy.addRequirement($2, $1) };
: requirementType requirementName STRUCT_START NEWLINE requirementBody { yy.addRequirement($2, $1) }
| requirementType requirementName STYLE_SEPARATOR idList STRUCT_START NEWLINE requirementBody { yy.addRequirement($2, $1); yy.setClass([$2], $4); }
;
requirementBody
: ID COLONSEP id NEWLINE requirementBody
@@ -149,8 +190,9 @@ verifyType
{ $$=yy.VerifyType.VERIFY_TEST;};
elementDef
: ELEMENT elementName STRUCT_START NEWLINE elementBody
{ yy.addElement($2) };
: ELEMENT elementName STRUCT_START NEWLINE elementBody { yy.addElement($2) }
| ELEMENT elementName STYLE_SEPARATOR idList STRUCT_START NEWLINE elementBody { yy.addElement($2); yy.setClass([$2], $4); }
;
elementBody
: TYPE COLONSEP type NEWLINE elementBody
@@ -182,6 +224,38 @@ relationship
| TRACES
{ $$=yy.Relationships.TRACES;};
classDefStatement
: CLASSDEF idList stylesOpt {$$ = $CLASSDEF;yy.defineClass($idList,$stylesOpt);}
;
classStatement
: CLASS idList idList {yy.setClass($2, $3);}
| id STYLE_SEPARATOR idList {yy.setClass([$1], $3);}
;
idList
: ALPHA { $$ = [$ALPHA]; }
| idList COMMA ALPHA = { $$ = $idList.concat([$ALPHA]); }
| id { $$ = [$id]; }
| idList COMMA id = { $$ = $idList.concat([$id]); }
;
styleStatement
: STYLE idList stylesOpt {$$ = $STYLE;yy.setCssStyle($2,$stylesOpt);}
;
stylesOpt
: style {$$ = [$style]}
| stylesOpt COMMA style {$stylesOpt.push($style);$$ = $stylesOpt;}
;
style
: styleComponent
| style styleComponent {$$ = $style + $styleComponent;}
;
styleComponent: ALPHA | NUM | COLON | UNIT | SPACE | BRKT | PCT | MINUS | LABEL | SEMICOLON;
requirementName: unqString | qString;
id : unqString | qString;

View File

@@ -1,5 +1,5 @@
import { setConfig } from '../../../config.js';
import requirementDb from '../requirementDb.js';
import { RequirementDB } from '../requirementDb.js';
import reqDiagram from './requirementDiagram.jison';
setConfig({
@@ -7,6 +7,7 @@ setConfig({
});
describe('when parsing requirement diagram it...', function () {
const requirementDb = new RequirementDB();
beforeEach(function () {
reqDiagram.parser.yy = requirementDb;
reqDiagram.parser.yy.clear();
@@ -37,7 +38,7 @@ describe('when parsing requirement diagram it...', function () {
let foundReq = requirementDb.getRequirements().get(expectedName);
expect(foundReq).toBeDefined();
expect(foundReq.id).toBe(expectedId);
expect(foundReq.requirementId).toBe(expectedId);
expect(foundReq.text).toBe(expectedText);
expect(requirementDb.getElements().size).toBe(0);
@@ -599,4 +600,251 @@ line 2`;
expect(reqDiagram.parser.yy.getElements().size).toBe(1);
});
}
it('will accept styling a requirement', function () {
const expectedName = 'test_req';
let lines = [
`requirementDiagram`,
``,
`requirement ${expectedName} {`,
`}`,
`style ${expectedName} fill:#f9f,stroke:#333,stroke-width:4px`,
``,
];
let doc = lines.join('\n');
reqDiagram.parser.parse(doc);
let foundReq = requirementDb.getRequirements().get(expectedName);
const styles = foundReq.cssStyles;
expect(styles).toEqual(['fill:#f9f', 'stroke:#333', 'stroke-width:4px']);
});
it('will accept styling an element', function () {
const expectedName = 'test_element';
let lines = [
`requirementDiagram`,
``,
`element ${expectedName} {`,
`}`,
`style ${expectedName} fill:#f9f,stroke:#333,stroke-width:4px`,
``,
];
let doc = lines.join('\n');
reqDiagram.parser.parse(doc);
let foundElement = requirementDb.getElements().get(expectedName);
const styles = foundElement.cssStyles;
expect(styles).toEqual(['fill:#f9f', 'stroke:#333', 'stroke-width:4px']);
});
it('will accept styling multiple things at once', function () {
const expectedRequirementName = 'test_requirement';
const expectedElementName = 'test_element';
let lines = [
`requirementDiagram`,
``,
`requirement ${expectedRequirementName} {`,
`}`,
`element ${expectedElementName} {`,
`}`,
`style ${expectedRequirementName},${expectedElementName} fill:#f9f,stroke:#333,stroke-width:4px`,
``,
];
let doc = lines.join('\n');
reqDiagram.parser.parse(doc);
let foundRequirement = requirementDb.getRequirements().get(expectedRequirementName);
const requirementStyles = foundRequirement.cssStyles;
expect(requirementStyles).toEqual(['fill:#f9f', 'stroke:#333', 'stroke-width:4px']);
let foundElement = requirementDb.getElements().get(expectedElementName);
const elementStyles = foundElement.cssStyles;
expect(elementStyles).toEqual(['fill:#f9f', 'stroke:#333', 'stroke-width:4px']);
});
it('will accept defining a class', function () {
const expectedName = 'myClass';
let lines = [
`requirementDiagram`,
``,
`classDef ${expectedName} fill:#f9f,stroke:#333,stroke-width:4px`,
``,
];
let doc = lines.join('\n');
reqDiagram.parser.parse(doc);
let foundClass = requirementDb.getClasses().get(expectedName);
expect(foundClass).toEqual({
id: 'myClass',
styles: ['fill:#f9f', 'stroke:#333', 'stroke-width:4px'],
textStyles: [],
});
});
it('will accept defining multiple classes at once', function () {
const firstName = 'firstClass';
const secondName = 'secondClass';
let lines = [
`requirementDiagram`,
``,
`classDef ${firstName},${secondName} fill:#f9f,stroke:#333,stroke-width:4px`,
``,
];
let doc = lines.join('\n');
reqDiagram.parser.parse(doc);
let firstClass = requirementDb.getClasses().get(firstName);
expect(firstClass).toEqual({
id: 'firstClass',
styles: ['fill:#f9f', 'stroke:#333', 'stroke-width:4px'],
textStyles: [],
});
let secondClass = requirementDb.getClasses().get(secondName);
expect(secondClass).toEqual({
id: 'secondClass',
styles: ['fill:#f9f', 'stroke:#333', 'stroke-width:4px'],
textStyles: [],
});
});
it('will accept assigning a class via the class statement', function () {
const requirementName = 'myReq';
const className = 'myClass';
let lines = [
`requirementDiagram`,
``,
`requirement ${requirementName} {`,
`}`,
`classDef ${className} fill:#f9f,stroke:#333,stroke-width:4px`,
`class ${requirementName} ${className}`,
``,
];
let doc = lines.join('\n');
reqDiagram.parser.parse(doc);
let foundRequirement = requirementDb.getRequirements().get(requirementName);
expect(foundRequirement.classes).toEqual(['default', className]);
});
it('will accept assigning multiple classes to multiple things via the class statement', function () {
const requirementName = 'req';
const elementName = 'elem';
const firstClassName = 'class1';
const secondClassName = 'class2';
let lines = [
`requirementDiagram`,
``,
`requirement ${requirementName} {`,
`}`,
`element ${elementName} {`,
`}`,
`classDef ${firstClassName},${secondClassName} fill:#f9f,stroke:#333,stroke-width:4px`,
`class ${requirementName},${elementName} ${firstClassName},${secondClassName}`,
``,
];
let doc = lines.join('\n');
reqDiagram.parser.parse(doc);
let requirement = requirementDb.getRequirements().get(requirementName);
expect(requirement.classes).toEqual(['default', firstClassName, secondClassName]);
let element = requirementDb.getElements().get(elementName);
expect(element.classes).toEqual(['default', firstClassName, secondClassName]);
});
it('will accept assigning a class via the shorthand syntax', function () {
const requirementName = 'myReq';
const className = 'myClass';
let lines = [
`requirementDiagram`,
``,
`requirement ${requirementName} {`,
`}`,
`classDef ${className} fill:#f9f,stroke:#333,stroke-width:4px`,
`${requirementName}:::${className}`,
``,
];
let doc = lines.join('\n');
reqDiagram.parser.parse(doc);
let foundRequirement = requirementDb.getRequirements().get(requirementName);
expect(foundRequirement.classes).toEqual(['default', className]);
});
it('will accept assigning multiple classes via the shorthand syntax', function () {
const requirementName = 'myReq';
const firstClassName = 'class1';
const secondClassName = 'class2';
let lines = [
`requirementDiagram`,
``,
`requirement ${requirementName} {`,
`}`,
`classDef ${firstClassName} fill:#f9f,stroke:#333,stroke-width:4px`,
`classDef ${secondClassName} color:blue`,
`${requirementName}:::${firstClassName},${secondClassName}`,
``,
];
let doc = lines.join('\n');
reqDiagram.parser.parse(doc);
let foundRequirement = requirementDb.getRequirements().get(requirementName);
expect(foundRequirement.classes).toEqual(['default', firstClassName, secondClassName]);
});
it('will accept assigning a class or multiple via the shorthand syntax when defining a requirement or element', function () {
const requirementName = 'myReq';
const elementName = 'myElem';
const firstClassName = 'class1';
const secondClassName = 'class2';
let lines = [
`requirementDiagram`,
``,
`requirement ${requirementName}:::${firstClassName} {`,
`}`,
`element ${elementName}:::${firstClassName},${secondClassName} {`,
`}`,
``,
`classDef ${firstClassName} fill:#f9f,stroke:#333,stroke-width:4px`,
`classDef ${secondClassName} color:blue`,
``,
];
let doc = lines.join('\n');
reqDiagram.parser.parse(doc);
let foundRequirement = requirementDb.getRequirements().get(requirementName);
expect(foundRequirement.classes).toEqual(['default', firstClassName]);
let foundElement = requirementDb.getElements().get(elementName);
expect(foundElement.classes).toEqual(['default', firstClassName, secondClassName]);
});
describe('will parse direction statements and', () => {
test.each(['TB', 'BT', 'LR', 'RL'])('will accept direction %s', (directionVal) => {
const lines = ['requirementDiagram', '', `direction ${directionVal}`, ''];
const doc = lines.join('\n');
reqDiagram.parser.parse(doc);
const direction = requirementDb.getDirection();
expect(direction).toBe(directionVal);
});
});
});

View File

@@ -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,
};

View File

@@ -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']);
});
});

View 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: `&lt;&lt;${relation.type}&gt;&gt;`,
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;
}

View File

@@ -1,13 +1,15 @@
import type { DiagramDefinition } from '../../diagram-api/types.js';
// @ts-ignore: JISON doesn't support types
import parser from './parser/requirementDiagram.jison';
import db from './requirementDb.js';
import { RequirementDB } from './requirementDb.js';
import styles from './styles.js';
import renderer from './requirementRenderer.js';
import * as renderer from './requirementRenderer.js';
export const diagram: DiagramDefinition = {
parser,
db,
get db() {
return new RequirementDB();
},
renderer,
styles,
};

View File

@@ -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,
};

View File

@@ -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,
};

View File

@@ -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);
};

View File

@@ -40,6 +40,21 @@ const getStyles = (options) => `
.relationshipLabel {
fill: ${options.relationLabelColor};
}
.divider {
stroke: ${options.nodeBorder};
stroke-width: 1;
}
.label {
font-family: ${options.fontFamily};
color: ${options.nodeTextColor || options.textColor};
}
.label text,span {
fill: ${options.nodeTextColor || options.textColor};
color: ${options.nodeTextColor || options.textColor};
}
.labelBkg {
background-color: ${options.edgeLabelBackground};
}
`;
// fill', conf.rect_fill)

View 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[];
}

View File

@@ -61,6 +61,26 @@ element user_defined_name {
}
```
### Markdown Formatting
In places where user defined text is possible (like names, requirement text, element docref, etc.), you can:
- Surround the text in quotes: `"example text"`
- Use markdown formatting inside quotes: `"**bold text** and *italics*"`
Example:
```mermaid-example
requirementDiagram
requirement "__test_req__" {
id: 1
text: "*italicized text* **bold text**"
risk: high
verifymethod: test
}
```
### Relationship
Relationships are comprised of a source node, destination node, and relationship type.
@@ -157,4 +177,140 @@ This example uses all features of the diagram.
test_req <- copies - test_entity2
```
## Direction
The diagram can be rendered in different directions using the `direction` statement. Valid values are:
- `TB` - Top to Bottom (default)
- `BT` - Bottom to Top
- `LR` - Left to Right
- `RL` - Right to Left
Example:
```mermaid-example
requirementDiagram
direction LR
requirement test_req {
id: 1
text: the test text.
risk: high
verifymethod: test
}
element test_entity {
type: simulation
}
test_entity - satisfies -> test_req
```
## Styling
Requirements and elements can be styled using direct styling or classes. As a rule of thumb, when applying styles or classes, it accepts a list of requirement or element names and a list of class names allowing multiple assignments at a time (The only exception is the shorthand syntax `:::` which can assign multiple classes but only to one requirement or element at a time).
### Direct Styling
Use the `style` keyword to apply CSS styles directly:
```mermaid-example
requirementDiagram
requirement test_req {
id: 1
text: styling example
risk: low
verifymethod: test
}
element test_entity {
type: simulation
}
style test_req fill:#ffa,stroke:#000, color: green
style test_entity fill:#f9f,stroke:#333, color: blue
```
### Class Definitions
Define reusable styles using `classDef`:
```mermaid-example
requirementDiagram
requirement test_req {
id: 1
text: "class styling example"
risk: low
verifymethod: test
}
element test_entity {
type: simulation
}
classDef important fill:#f96,stroke:#333,stroke-width:4px
classDef test fill:#ffa,stroke:#000
```
### Default class
If a class is named default it will be applied to all nodes. Specific styles and classes should be defined afterwards to override the applied default styling.
```
classDef default fill:#f9f,stroke:#333,stroke-width:4px;
```
### Applying Classes
Classes can be applied in two ways:
1. Using the `class` keyword:
```
class test_req,test_entity important
```
2. Using the shorthand syntax with `:::` either during the definition or afterwards:
```
requirement test_req:::important {
id: 1
text: class styling example
risk: low
verifymethod: test
}
```
```
element test_elem {
}
test_elem:::myClass
```
### Combined Example
```mermaid-example
requirementDiagram
requirement test_req:::important {
id: 1
text: "class styling example"
risk: low
verifymethod: test
}
element test_entity {
type: simulation
}
classDef important font-weight:bold
class test_entity important
style test_entity fill:#f9f,stroke:#333
```
<!--- cspell:ignore reqs --->

View File

@@ -39,6 +39,8 @@ const arrowTypesMap = {
zero_or_one: 'zeroOrOne',
one_or_more: 'oneOrMore',
zero_or_more: 'zeroOrMore',
requirement_arrow: 'requirement_arrow',
requirement_contains: 'requirement_contains',
} as const;
const addEdgeMarker = (

View File

@@ -398,6 +398,42 @@ const zero_or_more = (elem, type, id) => {
.attr('orient', 'auto');
endMarker.append('circle').attr('fill', 'white').attr('cx', 9).attr('cy', 18).attr('r', 6);
endMarker.append('path').attr('d', 'M21,18 Q39,0 57,18 Q39,36 21,18');
const requirement_arrow = (elem, type, id) => {
elem
.append('defs')
.append('marker')
.attr('id', id + '_' + type + '-requirement_arrowEnd')
.attr('refX', 20)
.attr('refY', 10)
.attr('markerWidth', 20)
.attr('markerHeight', 20)
.attr('orient', 'auto')
.append('path')
.attr(
'd',
`M0,0
L20,10
M20,10
L0,20`
);
};
const requirement_contains = (elem, type, id) => {
const containsNode = elem
.append('defs')
.append('marker')
.attr('id', id + '_' + type + '-requirement_containsEnd')
.attr('refX', 20)
.attr('refY', 10)
.attr('markerWidth', 20)
.attr('markerHeight', 20)
.attr('orient', 'auto')
.append('g');
containsNode.append('circle').attr('cx', 10).attr('cy', 10).attr('r', 10).attr('fill', 'none');
containsNode.append('line').attr('x1', 0).attr('x2', 20).attr('y1', 10).attr('y2', 10);
containsNode.append('line').attr('y1', 0).attr('y2', 20).attr('x1', 10).attr('x2', 10);
};
// TODO rename the class diagram markers to something shape descriptive and semantic free
@@ -415,5 +451,7 @@ const markers = {
zero_or_one,
one_or_more,
zero_or_more,
requirement_arrow,
requirement_contains,
};
export default insertMarkers;

View File

@@ -59,6 +59,7 @@ import { waveRectangle } from './shapes/waveRectangle.js';
import { windowPane } from './shapes/windowPane.js';
import { erBox } from './shapes/erBox.js';
import { classBox } from './shapes/classBox.js';
import { requirementBox } from './shapes/requirementBox.js';
import { kanbanItem } from './shapes/kanbanItem.js';
type ShapeHandler = <T extends SVGGraphicsElement>(
@@ -480,6 +481,9 @@ const generateShapeMap = () => {
// er diagram
erBox,
// Requirement diagram
requirementBox,
} as const;
const entries = [

View File

@@ -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,
`&lt;&lt;${requirementNode.type}&gt;&gt;`,
0,
node.labelStyle
);
} else {
typeHeight = await addText(shapeSvg, '&lt;&lt;Element&gt;&gt;', 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('&gt;', '>').replaceAll('&lt;', '<');
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;
}

View File

@@ -337,6 +337,9 @@ export const calculatePoint = (points: Point[], distanceToTraverse: number): Poi
for (const point of points) {
if (prevPoint) {
const vectorDistance = distance(point, prevPoint);
if (vectorDistance === 0) {
return prevPoint;
}
if (vectorDistance < remainingDistance) {
remainingDistance -= vectorDistance;
} else {