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 {
|
||||
fill: blue;
|
||||
/* font-weight: bold; */
|
||||
font-weight: 600;
|
||||
}
|
||||
text:hover {
|
||||
fill: darkcyan;
|
||||
|
|
@ -125,12 +125,13 @@ text:hover {
|
|||
|
||||
.highlight {
|
||||
stroke: green;
|
||||
stroke-width: 4px;
|
||||
stroke-width: 3px;
|
||||
}
|
||||
|
||||
.arrow.error {
|
||||
stroke: rgb(230,0,0);
|
||||
}
|
||||
text.error {
|
||||
text.error, tspan.error {
|
||||
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}
|
||||
className={
|
||||
(selection.find(s => s.uid === txt.uid)?.parts?.length ? "selected":"")
|
||||
+(textsToHighlight.hasOwnProperty(txt.uid)?" highlight":"")
|
||||
// +(errors.some(([uid]) => uid === txt.uid)?" error":"")
|
||||
}
|
||||
x={txt.topLeft.x}
|
||||
y={txt.topLeft.y}
|
||||
|
|
@ -501,8 +524,8 @@ export function VisualEditor() {
|
|||
}
|
||||
}}
|
||||
>
|
||||
{txt.text}
|
||||
</text>)}
|
||||
{markedText}
|
||||
</text>;})}
|
||||
|
||||
{selectingState && <Selecting {...selectingState} />}
|
||||
|
||||
|
|
@ -648,7 +671,9 @@ export function RountangleSVG(props: {rountangle: Rountangle, selected: string[]
|
|||
data-uid={uid}
|
||||
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>;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { TransitionLabel } from "./label_ast";
|
||||
|
||||
export type AbstractState = {
|
||||
uid: string;
|
||||
children: ConcreteState[];
|
||||
|
|
@ -20,37 +22,9 @@ export type Transition = {
|
|||
uid: string;
|
||||
src: ConcreteState;
|
||||
tgt: ConcreteState;
|
||||
trigger: Trigger;
|
||||
guard: Expression;
|
||||
actions: Action[];
|
||||
label: TransitionLabel[];
|
||||
}
|
||||
|
||||
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 = {
|
||||
root: ConcreteState;
|
||||
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 { findNearestRountangleSide, Rountangle, VisualEditorState } from "./editor_types";
|
||||
import { findNearestArrow, findNearestRountangleSide, Rountangle, VisualEditorState } from "./editor_types";
|
||||
import { isEntirelyWithin } from "./geometry";
|
||||
import { TransitionLabel } from "./label_ast";
|
||||
|
||||
import { parse as parseLabel } from "./label_parser";
|
||||
|
||||
export function parseStatechart(state: VisualEditorState): [Statechart, [string,string][]] {
|
||||
const errorShapes: [string, string][] = [];
|
||||
|
||||
// implicitly, the root is always an Or-state
|
||||
const root: OrState = {
|
||||
kind: "or",
|
||||
|
|
@ -23,6 +28,8 @@ export function parseStatechart(state: VisualEditorState): [Statechart, [string,
|
|||
|
||||
const parentLinks = new Map<string, string>();
|
||||
|
||||
// step 1: figure out state hierarchy
|
||||
|
||||
// we assume that the rountangles are sorted from big to small:
|
||||
for (const rt of state.rountangles) {
|
||||
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) {
|
||||
const srcUID = findNearestRountangleSide(arr, "start", state.rountangles)?.uid;
|
||||
|
|
@ -84,15 +92,12 @@ export function parseStatechart(state: VisualEditorState): [Statechart, [string,
|
|||
uid: arr.uid,
|
||||
src: uid2State.get(srcUID)!,
|
||||
tgt: uid2State.get(tgtUID)!,
|
||||
trigger: {
|
||||
kind: "?",
|
||||
},
|
||||
guard: {},
|
||||
actions: [],
|
||||
label: [],
|
||||
};
|
||||
const existingTransitions = transitions.get(srcUID) || [];
|
||||
existingTransitions.push(transition);
|
||||
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 [{
|
||||
root,
|
||||
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 {
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
font-family: Roboto, sans-serif;
|
||||
}
|
||||
|
||||
div#root {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue