Merge pull request #2917 from therzka/accessible-state-diagrams

feat: add accDescription field to state diagrams
This commit is contained in:
Knut Sveidqvist
2022-04-12 07:31:55 +02:00
committed by GitHub
8 changed files with 492 additions and 1 deletions

View File

@@ -680,6 +680,7 @@
<div class="mermaid"> <div class="mermaid">
stateDiagram stateDiagram
accDescription This is a state diagram showing one state
State1 State1
</div> </div>

50
demos/state.html Normal file
View File

@@ -0,0 +1,50 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Mermaid Quick Test Page</title>
<link rel="icon" type="image/png" href="data:image/png;base64,iVBORw0KGgo=">
<style>
div.mermaid {
/* font-family: 'trebuchet ms', verdana, arial; */
font-family: 'Courier New', Courier, monospace !important;
}
</style>
</head>
<body>
<div class="mermaid">
stateDiagram
title This is a title
accDescription This is an accessible description
State1
</div>
<div class="mermaid">
stateDiagram-v2
title This is a title
accDescription This is an accessible description
State1
</div>
<script src="./mermaid.js"></script>
<script>
mermaid.initialize({
theme: 'forest',
// themeCSS: '.node rect { fill: red; }',
logLevel: 3,
securityLevel: 'loose',
flowchart: { curve: 'basis' },
gantt: { axisFormat: '%m/%d/%Y' },
sequence: { actorMargin: 50 },
// sequenceDiagram: { actorMargin: 300 } // deprecated
});
</script>
</body>
</html>

View File

@@ -20,6 +20,8 @@
%x STATE_ID %x STATE_ID
%x ALIAS %x ALIAS
%x SCALE %x SCALE
%x title
%x accDescription
%x NOTE %x NOTE
%x NOTE_ID %x NOTE_ID
%x NOTE_TEXT %x NOTE_TEXT
@@ -58,6 +60,11 @@
<SCALE>\d+ return 'WIDTH'; <SCALE>\d+ return 'WIDTH';
<SCALE>\s+"width" {this.popState();} <SCALE>\s+"width" {this.popState();}
title { this.begin("title");return 'title'; }
<title>(?!\n|;|#)*[^\n]* { this.popState(); return "title_value"; }
accDescription { this.begin("accDescription");return 'accDescription'; }
<accDescription>(?!\n|;|#)*[^\n]* { this.popState(); return "description_value"; }
<INITIAL,struct>"state"\s+ { /*console.log('Starting STATE zxzx'+yy.getDirection());*/this.pushState('STATE'); } <INITIAL,struct>"state"\s+ { /*console.log('Starting STATE zxzx'+yy.getDirection());*/this.pushState('STATE'); }
<STATE>.*"<<fork>>" {this.popState();yytext=yytext.slice(0,-8).trim(); /*console.warn('Fork Fork: ',yytext);*/return 'FORK';} <STATE>.*"<<fork>>" {this.popState();yytext=yytext.slice(0,-8).trim(); /*console.warn('Fork Fork: ',yytext);*/return 'FORK';}
<STATE>.*"<<join>>" {this.popState();yytext=yytext.slice(0,-8).trim();/*console.warn('Fork Join: ',yytext);*/return 'JOIN';} <STATE>.*"<<join>>" {this.popState();yytext=yytext.slice(0,-8).trim();/*console.warn('Fork Join: ',yytext);*/return 'JOIN';}
@@ -193,6 +200,8 @@ statement
| note NOTE_TEXT AS ID | note NOTE_TEXT AS ID
| directive | directive
| direction | direction
| title title_value { $$=$2.trim();yy.setTitle($$); }
| accDescription description_value { $$=$2.trim();yy.setAccDescription($$); }
; ;
directive directive

View File

@@ -4,6 +4,8 @@ import mermaidAPI from '../../mermaidAPI';
import common from '../common/common'; import common from '../common/common';
import * as configApi from '../../config'; import * as configApi from '../../config';
const sanitizeText = (txt) => common.sanitizeText(txt, configApi.getConfig());
const clone = (o) => JSON.parse(JSON.stringify(o)); const clone = (o) => JSON.parse(JSON.stringify(o));
let rootDoc = []; let rootDoc = [];
@@ -115,6 +117,25 @@ let startCnt = 0;
let endCnt = 0; // eslint-disable-line let endCnt = 0; // eslint-disable-line
// let stateCnt = 0; // let stateCnt = 0;
let title = 'State diagram';
let description = '';
const setTitle = function (txt) {
title = sanitizeText(txt);
};
const getTitle = function () {
return title;
};
const setAccDescription = function (txt) {
description = sanitizeText(txt);
};
const getAccDescription = function () {
return description;
};
/** /**
* Function called by parser when a node definition has been found. * Function called by parser when a node definition has been found.
* *
@@ -280,4 +301,8 @@ export default {
getRootDocV2, getRootDocV2,
extract, extract,
trimColon, trimColon,
getTitle,
setTitle,
getAccDescription,
setAccDescription,
}; };

View File

@@ -0,0 +1,376 @@
import { parser } from './parser/stateDiagram';
import stateDb from './stateDb';
describe('state diagram, ', function () {
describe('when parsing an info graph it', function () {
beforeEach(function () {
parser.yy = stateDb;
});
it('super simple', function () {
const str = `
stateDiagram-v2
[*] --> State1
State1 --> [*]
`;
parser.parse(str);
});
it('simple', function () {
const str = `stateDiagram-v2\n
State1 : this is another string
[*] --> State1
State1 --> [*]
`;
parser.parse(str);
const description = stateDb.getAccDescription();
expect(description).toBe('');
});
it('simple with accDescription', function () {
const str = `stateDiagram-v2\n
accDescription a simple description of the diagram
State1 : this is another string
[*] --> State1
State1 --> [*]
`;
parser.parse(str);
const description = stateDb.getAccDescription();
expect(description).toBe('a simple description of the diagram');
});
it('simple with title', function () {
const str = `stateDiagram-v2\n
title a simple title of the diagram
State1 : this is another string
[*] --> State1
State1 --> [*]
`;
parser.parse(str);
const title = stateDb.getTitle();
expect(title).toBe('a simple title of the diagram');
});
it('simple with directive', function () {
const str = `%%{init: {'logLevel': 0 }}%%
stateDiagram-v2\n
State1 : this is another string
[*] --> State1
State1 --> [*]
`;
parser.parse(str);
});
it('should handle relation definitions', function () {
const str = `stateDiagram-v2\n
[*] --> State1
State1 --> [*]
State1 : this is a string
State1 : this is another string
State1 --> State2
State2 --> [*]
`;
parser.parse(str);
});
it('hide empty description', function () {
const str = `stateDiagram-v2\n
hide empty description
[*] --> State1
State1 --> [*]
State1 : this is a string
State1 : this is another string
State1 --> State2
State2 --> [*]
`;
parser.parse(str);
});
it('handle "as" in state names', function () {
const str = `stateDiagram-v2
assemble
state assemble
`;
parser.parse(str);
});
it('handle "as" in state names 1', function () {
const str = `stateDiagram-v2
assemble
state assemble
`;
parser.parse(str);
});
it('handle "as" in state names 2', function () {
const str = `stateDiagram-v2
assembleas
state assembleas
`;
parser.parse(str);
});
it('handle "as" in state names 3', function () {
const str = `stateDiagram-v2
state "as" as as
`;
parser.parse(str);
});
it('scale', function () {
const str = `stateDiagram-v2\n
scale 350 width
[*] --> State1
State1 --> [*]
State1 : this is a string with - in it
State1 : this is another string
State1 --> State2
State2 --> [*]
`;
parser.parse(str);
});
it('description after second state', function () {
const str = `stateDiagram-v2\n
scale 350 width
[*] --> State1 : This is the description with - in it
State1 --> [*]
`;
parser.parse(str);
})
it('shall handle descriptions including minus signs', function () {
const str = `stateDiagram-v2\n
scale 350 width
[*] --> State1 : This is the description +-!
State1 --> [*]
`;
parser.parse(str);
});
it('should handle state statements', function () {
const str = `stateDiagram-v2\n
state Configuring {
[*] --> NewValueSelection
NewValueSelection --> NewValuePreview : EvNewValue
NewValuePreview --> NewValueSelection : EvNewValueRejected
NewValuePreview --> NewValueSelection : EvNewValueSaved1
}
`;
parser.parse(str);
});
it('should handle recursive state definitions', function () {
const str = `stateDiagram-v2\n
state Configuring {
[*] --> NewValueSelection
NewValueSelection --> NewValuePreview : EvNewValue
NewValuePreview --> NewValueSelection : EvNewValueRejected
NewValuePreview --> NewValueSelection : EvNewValueSaved
state NewValuePreview {
State1 --> State2
}
}
`;
parser.parse(str);
});
it('should handle multiple recursive state definitions', function () {
const str = `stateDiagram-v2\n
scale 350 width
[*] --> NotShooting
state NotShooting {
[*] --> Idle
Idle --> Configuring : EvConfig
Configuring --> Idle : EvConfig
}
state Configuring {
[*] --> NewValueSelection
NewValueSelection --> NewValuePreview : EvNewValue
NewValuePreview --> NewValueSelection : EvNewValueRejected
NewValuePreview --> NewValueSelection : EvNewValueSaved
state NewValuePreview {
State1 --> State2
}
}
`;
parser.parse(str);
});
it('should handle state deifintions with separation of id', function () {
const str = `stateDiagram-v2\n
state "Long state description" as state1
`;
parser.parse(str);
});
it('should handle state deifintions with separation of id', function () {
const str = `stateDiagram-v2
state "Not Shooting State" as NotShooting {
state "Idle mode" as Idle
state "Configuring mode" as Configuring
[*] --> Idle
Idle --> Configuring : EvConfig
Configuring --> Idle : EvConfig
}
`;
parser.parse(str);
});
it('should State definition with quotes', function () {
const str = `stateDiagram-v2\n
scale 600 width
[*] --> State1
State1 --> State2 : Succeeded
State1 --> [*] : Aborted
State2 --> State3 : Succeeded
State2 --> [*] : Aborted
state State3 {
state "Accumulate Enough Data\nLong State Name" as long1
long1 : Just a test
[*] --> long1
long1 --> long1 : New Data
long1 --> ProcessData : Enough Data
}
State3 --> State3 : Failed
State3 --> [*] : Succeeded / Save Result
State3 --> [*] : Aborted
`;
parser.parse(str);
});
it('should handle fork statements', function () {
const str = `stateDiagram-v2\n
state fork_state <<fork>>
[*] --> fork_state
fork_state --> State2
fork_state --> State3
state join_state <<join>>
State2 --> join_state
State3 --> join_state
join_state --> State4
State4 --> [*]
`;
parser.parse(str);
});
it('should handle concurrent state', function () {
const str = `stateDiagram-v2\n
[*] --> Active
state Active {
[*] --> NumLockOff
NumLockOff --> NumLockOn : EvNumLockPressed
NumLockOn --> NumLockOff : EvNumLockPressed
--
[*] --> CapsLockOff
CapsLockOff --> CapsLockOn : EvCapsLockPressed
CapsLockOn --> CapsLockOff : EvCapsLockPressed
--
[*] --> ScrollLockOff
ScrollLockOff --> ScrollLockOn : EvCapsLockPressed
ScrollLockOn --> ScrollLockOff : EvCapsLockPressed
}
`;
parser.parse(str);
});
it('should handle concurrent state', function () {
const str = `stateDiagram-v2\n
[*] --> Active
state Active {
[*] --> NumLockOff
--
[*] --> CapsLockOff
--
[*] --> ScrollLockOff
}
`;
parser.parse(str);
});
// it('should handle arrow directions definitions', function() {
// const str = `stateDiagram-v2\n
// [*] -up-> First
// First -right-> Second
// Second --> Third
// Third -left-> Last
// `;
// parser.parse(str);
// });
it('should handle note statements', function () {
const str = `stateDiagram-v2\n
[*] --> Active
Active --> Inactive
note left of Active : this is a short<br/>note
note right of Inactive
A note can also
be defined on
several lines
end note
`;
parser.parse(str);
});
it('should handle multiline notes with different line breaks', function () {
const str = `stateDiagram-v2
State1
note right of State1
Line1<br>Line2<br/>Line3<br />Line4<br />Line5
end note
`;
parser.parse(str);
});
it('should handle floating notes', function () {
const str = `stateDiagram-v2
foo: bar
note "This is a floating note" as N1
`;
parser.parse(str);
});
it('should handle floating notes', function () {
const str = `stateDiagram-v2\n
state foo
note "This is a floating note" as N1
`;
parser.parse(str);
});
it('should handle notes for composit states', function () {
const str = `stateDiagram-v2\n
[*] --> NotShooting
state "Not Shooting State" as NotShooting {
state "Idle mode" as Idle
state "Configuring mode" as Configuring
[*] --> Idle
Idle --> Configuring : EvConfig
Configuring --> Idle : EvConfig
}
note right of NotShooting : This is a note on a composite state
`;
parser.parse(str);
});
});
});

View File

@@ -24,6 +24,32 @@ describe('state diagram, ', function () {
`; `;
parser.parse(str); parser.parse(str);
const description = stateDb.getAccDescription();
expect(description).toBe('');
});
it('simple with accDescription', function () {
const str = `stateDiagram\n
accDescription a simple description of the diagram
State1 : this is another string
[*] --> State1
State1 --> [*]
`;
parser.parse(str);
const description = stateDb.getAccDescription();
expect(description).toBe('a simple description of the diagram');
});
it('simple with title', function () {
const str = `stateDiagram\n
title a simple title of the diagram
State1 : this is another string
[*] --> State1
State1 --> [*]
`;
parser.parse(str);
const title = stateDb.getTitle();
expect(title).toBe('a simple title of the diagram');
}); });
it('simple with directive', function () { it('simple with directive', function () {
const str = `%%{init: {'logLevel': 0 }}%% const str = `%%{init: {'logLevel': 0 }}%%
@@ -119,7 +145,7 @@ describe('state diagram, ', function () {
parser.parse(str); parser.parse(str);
}); });
it('shall handle descriptions inkluding minus signs', function () { it('shall handle descriptions including minus signs', function () {
const str = `stateDiagram\n const str = `stateDiagram\n
scale 350 width scale 350 width
[*] --> State1 : This is the description +-! [*] --> State1 : This is the description +-!

View File

@@ -7,6 +7,7 @@ import { render } from '../../dagre-wrapper/index.js';
import { log } from '../../logger'; import { log } from '../../logger';
import { configureSvgSize } from '../../utils'; import { configureSvgSize } from '../../utils';
import common from '../common/common'; import common from '../common/common';
import addSVGAccessibilityFields from '../../accessibility';
const conf = {}; const conf = {};
export const setConf = function (cnf) { export const setConf = function (cnf) {
@@ -336,6 +337,7 @@ export const draw = function (text, id) {
label.insertBefore(rect, label.firstChild); label.insertBefore(rect, label.firstChild);
// } // }
} }
addSVGAccessibilityFields(parser.yy, svg, id);
}; };
export default { export default {

View File

@@ -9,6 +9,7 @@ import { parser } from './parser/stateDiagram';
import { drawState, addTitleAndBox, drawEdge } from './shapes'; import { drawState, addTitleAndBox, drawEdge } from './shapes';
import { getConfig } from '../../config'; import { getConfig } from '../../config';
import { configureSvgSize } from '../../utils'; import { configureSvgSize } from '../../utils';
import addSVGAccessibilityFields from '../../accessibility';
parser.yy = stateDb; parser.yy = stateDb;
@@ -97,6 +98,7 @@ export const draw = function (text, id) {
'viewBox', 'viewBox',
`${bounds.x - conf.padding} ${bounds.y - conf.padding} ` + width + ' ' + height `${bounds.x - conf.padding} ${bounds.y - conf.padding} ` + width + ' ' + height
); );
addSVGAccessibilityFields(parser.yy, diagram, id);
}; };
const getLabelWidth = (text) => { const getLabelWidth = (text) => {
return text ? text.length * conf.fontSizeFactor : 1; return text ? text.length * conf.fontSizeFactor : 1;