add transition label parser
This commit is contained in:
parent
58a75ddd8b
commit
e009f718d2
8 changed files with 1399 additions and 74 deletions
|
|
@ -116,7 +116,7 @@ line.selected, circle.selected {
|
||||||
|
|
||||||
text.selected, text.selected:hover {
|
text.selected, text.selected:hover {
|
||||||
fill: blue;
|
fill: blue;
|
||||||
/* font-weight: bold; */
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
text:hover {
|
text:hover {
|
||||||
fill: darkcyan;
|
fill: darkcyan;
|
||||||
|
|
@ -125,12 +125,13 @@ text:hover {
|
||||||
|
|
||||||
.highlight {
|
.highlight {
|
||||||
stroke: green;
|
stroke: green;
|
||||||
stroke-width: 4px;
|
stroke-width: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.arrow.error {
|
.arrow.error {
|
||||||
stroke: rgb(230,0,0);
|
stroke: rgb(230,0,0);
|
||||||
}
|
}
|
||||||
text.error {
|
text.error, tspan.error {
|
||||||
fill: rgb(230,0,0);
|
fill: rgb(230,0,0);
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
@ -470,11 +470,34 @@ export function VisualEditor() {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{state.texts.map(txt => <text
|
{state.texts.map(txt => {
|
||||||
|
const err = errors.find(([uid]) => txt.uid === uid)?.[1];
|
||||||
|
let markedText;
|
||||||
|
if (err) {
|
||||||
|
const {start,end} = err.location;
|
||||||
|
markedText = <>
|
||||||
|
{txt.text.slice(0, start.offset)}
|
||||||
|
<tspan className="error" data-uid={txt.uid} data-parts="text">
|
||||||
|
{txt.text.slice(start.offset, end.offset)}
|
||||||
|
</tspan>
|
||||||
|
{txt.text.slice(end.offset)}
|
||||||
|
</>;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
markedText = <>{txt.text}</>;
|
||||||
|
}
|
||||||
|
// const annotatedText = err ? [...txt.text].map((char,i) => {
|
||||||
|
// if (i >= err.location.start.offset && i < err.location.end.offset) {
|
||||||
|
// return char+'\u0332';
|
||||||
|
// }
|
||||||
|
// return char;
|
||||||
|
// }).join('') : txt.text;
|
||||||
|
return <text
|
||||||
key={txt.uid}
|
key={txt.uid}
|
||||||
className={
|
className={
|
||||||
(selection.find(s => s.uid === txt.uid)?.parts?.length ? "selected":"")
|
(selection.find(s => s.uid === txt.uid)?.parts?.length ? "selected":"")
|
||||||
+(textsToHighlight.hasOwnProperty(txt.uid)?" highlight":"")
|
+(textsToHighlight.hasOwnProperty(txt.uid)?" highlight":"")
|
||||||
|
// +(errors.some(([uid]) => uid === txt.uid)?" error":"")
|
||||||
}
|
}
|
||||||
x={txt.topLeft.x}
|
x={txt.topLeft.x}
|
||||||
y={txt.topLeft.y}
|
y={txt.topLeft.y}
|
||||||
|
|
@ -501,8 +524,8 @@ export function VisualEditor() {
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{txt.text}
|
{markedText}
|
||||||
</text>)}
|
</text>;})}
|
||||||
|
|
||||||
{selectingState && <Selecting {...selectingState} />}
|
{selectingState && <Selecting {...selectingState} />}
|
||||||
|
|
||||||
|
|
@ -648,7 +671,9 @@ export function RountangleSVG(props: {rountangle: Rountangle, selected: string[]
|
||||||
data-uid={uid}
|
data-uid={uid}
|
||||||
data-parts="bottom left"
|
data-parts="bottom left"
|
||||||
/>
|
/>
|
||||||
<text x={10} y={20}>{uid}</text>
|
<text x={10} y={20}
|
||||||
|
data-uid={uid}
|
||||||
|
data-parts="left top right bottom">{uid}</text>
|
||||||
</g>;
|
</g>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { TransitionLabel } from "./label_ast";
|
||||||
|
|
||||||
export type AbstractState = {
|
export type AbstractState = {
|
||||||
uid: string;
|
uid: string;
|
||||||
children: ConcreteState[];
|
children: ConcreteState[];
|
||||||
|
|
@ -20,37 +22,9 @@ export type Transition = {
|
||||||
uid: string;
|
uid: string;
|
||||||
src: ConcreteState;
|
src: ConcreteState;
|
||||||
tgt: ConcreteState;
|
tgt: ConcreteState;
|
||||||
trigger: Trigger;
|
label: TransitionLabel[];
|
||||||
guard: Expression;
|
|
||||||
actions: Action[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type EventTrigger = {
|
|
||||||
kind: "event";
|
|
||||||
event: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type AfterTrigger = {
|
|
||||||
kind: "after";
|
|
||||||
delay_ms: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Trigger = EventTrigger | AfterTrigger;
|
|
||||||
|
|
||||||
export type RaiseEvent = {
|
|
||||||
kind: "raise";
|
|
||||||
event: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Assign = {
|
|
||||||
lhs: string;
|
|
||||||
rhs: Expression;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Expression = {};
|
|
||||||
|
|
||||||
export type Action = RaiseEvent | Assign;
|
|
||||||
|
|
||||||
export type Statechart = {
|
export type Statechart = {
|
||||||
root: ConcreteState;
|
root: ConcreteState;
|
||||||
transitions: Map<string, Transition[]>; // key: source state uid
|
transitions: Map<string, Transition[]>; // key: source state uid
|
||||||
|
|
|
||||||
53
src/VisualEditor/label_ast.ts
Normal file
53
src/VisualEditor/label_ast.ts
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
export type TransitionLabel = {
|
||||||
|
trigger: Trigger;
|
||||||
|
guard: Expression;
|
||||||
|
actions: Action[];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export type Trigger = EventTrigger | AfterTrigger;
|
||||||
|
|
||||||
|
export type EventTrigger = {
|
||||||
|
kind: "event";
|
||||||
|
event: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AfterTrigger = {
|
||||||
|
kind: "after";
|
||||||
|
durationMs: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export type Action = Assignment | RaiseEvent;
|
||||||
|
|
||||||
|
export type Assignment = {
|
||||||
|
kind: "assignment";
|
||||||
|
lhs: string;
|
||||||
|
rhs: Expression;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RaiseEvent = {
|
||||||
|
kind: "raise";
|
||||||
|
event: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export type Expression = BinaryExpression | UnaryExpression | VarRef;
|
||||||
|
|
||||||
|
export type BinaryExpression = {
|
||||||
|
kind: "binaryExpr";
|
||||||
|
operator: "+" | "-" | "*" | "/" | "&&" | "||";
|
||||||
|
lhs: Expression;
|
||||||
|
rhs: Expression;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UnaryExpression = {
|
||||||
|
kind: "unaryExpr";
|
||||||
|
operator: "!" | "-";
|
||||||
|
expr: Expression;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type VarRef = {
|
||||||
|
kind: "ref";
|
||||||
|
variable: string;
|
||||||
|
}
|
||||||
1141
src/VisualEditor/label_parser.js
Normal file
1141
src/VisualEditor/label_parser.js
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1,8 +1,13 @@
|
||||||
import { ConcreteState, OrState, Statechart, Transition } from "./ast";
|
import { ConcreteState, OrState, Statechart, Transition } from "./ast";
|
||||||
import { findNearestRountangleSide, Rountangle, VisualEditorState } from "./editor_types";
|
import { findNearestArrow, findNearestRountangleSide, Rountangle, VisualEditorState } from "./editor_types";
|
||||||
import { isEntirelyWithin } from "./geometry";
|
import { isEntirelyWithin } from "./geometry";
|
||||||
|
import { TransitionLabel } from "./label_ast";
|
||||||
|
|
||||||
|
import { parse as parseLabel } from "./label_parser";
|
||||||
|
|
||||||
export function parseStatechart(state: VisualEditorState): [Statechart, [string,string][]] {
|
export function parseStatechart(state: VisualEditorState): [Statechart, [string,string][]] {
|
||||||
|
const errorShapes: [string, string][] = [];
|
||||||
|
|
||||||
// implicitly, the root is always an Or-state
|
// implicitly, the root is always an Or-state
|
||||||
const root: OrState = {
|
const root: OrState = {
|
||||||
kind: "or",
|
kind: "or",
|
||||||
|
|
@ -23,6 +28,8 @@ export function parseStatechart(state: VisualEditorState): [Statechart, [string,
|
||||||
|
|
||||||
const parentLinks = new Map<string, string>();
|
const parentLinks = new Map<string, string>();
|
||||||
|
|
||||||
|
// step 1: figure out state hierarchy
|
||||||
|
|
||||||
// we assume that the rountangles are sorted from big to small:
|
// we assume that the rountangles are sorted from big to small:
|
||||||
for (const rt of state.rountangles) {
|
for (const rt of state.rountangles) {
|
||||||
const state: ConcreteState = {
|
const state: ConcreteState = {
|
||||||
|
|
@ -49,9 +56,10 @@ export function parseStatechart(state: VisualEditorState): [Statechart, [string,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const transitions = new Map<string, Transition[]>();
|
// step 2: figure out transitions
|
||||||
|
|
||||||
const errorShapes: [string, string][] = [];
|
const transitions = new Map<string, Transition[]>();
|
||||||
|
const uid2Transition = new Map<string, Transition>();
|
||||||
|
|
||||||
for (const arr of state.arrows) {
|
for (const arr of state.arrows) {
|
||||||
const srcUID = findNearestRountangleSide(arr, "start", state.rountangles)?.uid;
|
const srcUID = findNearestRountangleSide(arr, "start", state.rountangles)?.uid;
|
||||||
|
|
@ -84,15 +92,12 @@ export function parseStatechart(state: VisualEditorState): [Statechart, [string,
|
||||||
uid: arr.uid,
|
uid: arr.uid,
|
||||||
src: uid2State.get(srcUID)!,
|
src: uid2State.get(srcUID)!,
|
||||||
tgt: uid2State.get(tgtUID)!,
|
tgt: uid2State.get(tgtUID)!,
|
||||||
trigger: {
|
label: [],
|
||||||
kind: "?",
|
|
||||||
},
|
|
||||||
guard: {},
|
|
||||||
actions: [],
|
|
||||||
};
|
};
|
||||||
const existingTransitions = transitions.get(srcUID) || [];
|
const existingTransitions = transitions.get(srcUID) || [];
|
||||||
existingTransitions.push(transition);
|
existingTransitions.push(transition);
|
||||||
transitions.set(srcUID, existingTransitions);
|
transitions.set(srcUID, existingTransitions);
|
||||||
|
uid2Transition.set(arr.uid, transition);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -108,6 +113,36 @@ export function parseStatechart(state: VisualEditorState): [Statechart, [string,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// step 3: figure out labels
|
||||||
|
|
||||||
|
for (const text of state.texts) {
|
||||||
|
const belongsToArrow = findNearestArrow(text.topLeft, state.arrows);
|
||||||
|
if (belongsToArrow) {
|
||||||
|
const belongsToTransition = uid2Transition.get(belongsToArrow.uid);
|
||||||
|
if (belongsToTransition) {
|
||||||
|
// parse as transition label
|
||||||
|
let transitionLabel: TransitionLabel;
|
||||||
|
try {
|
||||||
|
transitionLabel = parseLabel(text.text); // may throw
|
||||||
|
belongsToTransition.label.push(transitionLabel);
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
console.log({e});
|
||||||
|
errorShapes.push([text.uid, e]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const transition of uid2Transition.values()) {
|
||||||
|
if (transition.label.length === 0) {
|
||||||
|
errorShapes.push([transition.uid, "no label"]);
|
||||||
|
}
|
||||||
|
else if (transition.label.length > 1) {
|
||||||
|
errorShapes.push([transition.uid, "multiple labels"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return [{
|
return [{
|
||||||
root,
|
root,
|
||||||
transitions,
|
transitions,
|
||||||
|
|
|
||||||
95
src/VisualEditor/transition_label.grammar
Normal file
95
src/VisualEditor/transition_label.grammar
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
start = _ trigger:trigger _ guard:("[" _ guard _ "]")? _ actions:("/" _ actions )? _ {
|
||||||
|
return {
|
||||||
|
trigger,
|
||||||
|
guard: guard ? guard[2] : {kind: "literal", value: true},
|
||||||
|
actions: actions ? actions[2] : [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
trigger = afterTrigger / eventTrigger
|
||||||
|
|
||||||
|
eventTrigger = event:identifier {
|
||||||
|
return {kind: "event", event};
|
||||||
|
}
|
||||||
|
|
||||||
|
afterTrigger = "after" _ dur:durationMs {
|
||||||
|
return {kind: "after", durationMs: dur};
|
||||||
|
}
|
||||||
|
|
||||||
|
durationMs = num:number _ u:timeUnit {
|
||||||
|
return num * (u === "s" ? 1000 : 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
timeUnit = "ms" / "s" {
|
||||||
|
return text();
|
||||||
|
}
|
||||||
|
|
||||||
|
guard = expr
|
||||||
|
|
||||||
|
actions = head:action tail:(_ ";" _ action)* _ ";"? {
|
||||||
|
return [head, ...tail.map(t => t[3])];
|
||||||
|
}
|
||||||
|
action = assignment / raise
|
||||||
|
|
||||||
|
assignment = lhs:identifier _ "=" _ rhs:expr {
|
||||||
|
return {kind: "assignment", lhs, rhs};
|
||||||
|
}
|
||||||
|
|
||||||
|
identifier = [a-zA-Z0-9]+ {
|
||||||
|
return text();
|
||||||
|
}
|
||||||
|
|
||||||
|
number = [0-9]+ {
|
||||||
|
return parseInt(text());
|
||||||
|
}
|
||||||
|
|
||||||
|
_ "whitespace"
|
||||||
|
= [ \t\n\r]*
|
||||||
|
|
||||||
|
expr = sum
|
||||||
|
|
||||||
|
sum = prod:product rest:((_ ("+" / "-") _) sum)? {
|
||||||
|
if (rest === null) {
|
||||||
|
return prod;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
kind:"binaryExpr",
|
||||||
|
operator: rest[0][1],
|
||||||
|
lhs: prod,
|
||||||
|
rhs: rest[1],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
product = atom:atom rest:((_ ("*" / "/") _) product)? {
|
||||||
|
if (rest === null) {
|
||||||
|
return atom;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
kind:"binaryExpr",
|
||||||
|
operator: rest[0][1],
|
||||||
|
lhs: atom,
|
||||||
|
rhs: rest[1],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
atom = nested / literal / ref
|
||||||
|
|
||||||
|
nested = "(" _ expr:expr _ ")" {
|
||||||
|
return expr;
|
||||||
|
}
|
||||||
|
|
||||||
|
literal = value:(number / boolean) {
|
||||||
|
return {kind: "literal", value}
|
||||||
|
}
|
||||||
|
|
||||||
|
ref = variable:identifier {
|
||||||
|
return {kind: "ref", variable}
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean = ("true" / "false") {
|
||||||
|
return text() === "true";
|
||||||
|
}
|
||||||
|
|
||||||
|
raise = "^" _ event:identifier {
|
||||||
|
return {kind: "raise", event};
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
html, body {
|
html, body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
font-family: Roboto, sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
div#root {
|
div#root {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue