diff --git a/src/VisualEditor/VisualEditor.css b/src/VisualEditor/VisualEditor.css
index 2ad61ca..5b52cc4 100644
--- a/src/VisualEditor/VisualEditor.css
+++ b/src/VisualEditor/VisualEditor.css
@@ -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);
-}
\ No newline at end of file
+ font-weight: 600;
+}
diff --git a/src/VisualEditor/VisualEditor.tsx b/src/VisualEditor/VisualEditor.tsx
index 39ed002..951ef32 100644
--- a/src/VisualEditor/VisualEditor.tsx
+++ b/src/VisualEditor/VisualEditor.tsx
@@ -470,39 +470,62 @@ export function VisualEditor() {
/>
)}
- {state.texts.map(txt => s.uid === txt.uid)?.parts?.length ? "selected":"")
- +(textsToHighlight.hasOwnProperty(txt.uid)?" highlight":"")
+ {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)}
+
+ {txt.text.slice(start.offset, end.offset)}
+
+ {txt.text.slice(end.offset)}
+ >;
}
- x={txt.topLeft.x}
- y={txt.topLeft.y}
- textAnchor="middle"
- data-uid={txt.uid}
- data-parts="text"
- onDoubleClick={() => {
- const newText = prompt("", txt.text);
- if (newText) {
- setState(state => ({
- ...state,
- texts: state.texts.map(t => {
- if (t.uid === txt.uid) {
- return {
- ...txt,
- text: newText,
- }
- }
- else {
- return t;
- }
- }),
- }));
+ 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 s.uid === txt.uid)?.parts?.length ? "selected":"")
+ +(textsToHighlight.hasOwnProperty(txt.uid)?" highlight":"")
+ // +(errors.some(([uid]) => uid === txt.uid)?" error":"")
}
- }}
- >
- {txt.text}
- )}
+ x={txt.topLeft.x}
+ y={txt.topLeft.y}
+ textAnchor="middle"
+ data-uid={txt.uid}
+ data-parts="text"
+ onDoubleClick={() => {
+ const newText = prompt("", txt.text);
+ if (newText) {
+ setState(state => ({
+ ...state,
+ texts: state.texts.map(t => {
+ if (t.uid === txt.uid) {
+ return {
+ ...txt,
+ text: newText,
+ }
+ }
+ else {
+ return t;
+ }
+ }),
+ }));
+ }
+ }}
+ >
+ {markedText}
+ ;})}
{selectingState && }
@@ -648,7 +671,9 @@ export function RountangleSVG(props: {rountangle: Rountangle, selected: string[]
data-uid={uid}
data-parts="bottom left"
/>
- {uid}
+ {uid}
;
}
diff --git a/src/VisualEditor/ast.ts b/src/VisualEditor/ast.ts
index caf1f87..72be756 100644
--- a/src/VisualEditor/ast.ts
+++ b/src/VisualEditor/ast.ts
@@ -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; // key: source state uid
diff --git a/src/VisualEditor/label_ast.ts b/src/VisualEditor/label_ast.ts
new file mode 100644
index 0000000..a7b9579
--- /dev/null
+++ b/src/VisualEditor/label_ast.ts
@@ -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;
+}
diff --git a/src/VisualEditor/label_parser.js b/src/VisualEditor/label_parser.js
new file mode 100644
index 0000000..b275b41
--- /dev/null
+++ b/src/VisualEditor/label_parser.js
@@ -0,0 +1,1141 @@
+// @generated by Peggy 5.0.6.
+//
+// https://peggyjs.org/
+
+
+class peg$SyntaxError extends SyntaxError {
+ constructor(message, expected, found, location) {
+ super(message);
+ this.expected = expected;
+ this.found = found;
+ this.location = location;
+ this.name = "SyntaxError";
+ }
+
+ format(sources) {
+ let str = "Error: " + this.message;
+ if (this.location) {
+ let src = null;
+ const st = sources.find(s => s.source === this.location.source);
+ if (st) {
+ src = st.text.split(/\r\n|\n|\r/g);
+ }
+ const s = this.location.start;
+ const offset_s = (this.location.source && (typeof this.location.source.offset === "function"))
+ ? this.location.source.offset(s)
+ : s;
+ const loc = this.location.source + ":" + offset_s.line + ":" + offset_s.column;
+ if (src) {
+ const e = this.location.end;
+ const filler = "".padEnd(offset_s.line.toString().length, " ");
+ const line = src[s.line - 1];
+ const last = s.line === e.line ? e.column : line.length + 1;
+ const hatLen = (last - s.column) || 1;
+ str += "\n --> " + loc + "\n"
+ + filler + " |\n"
+ + offset_s.line + " | " + line + "\n"
+ + filler + " | " + "".padEnd(s.column - 1, " ")
+ + "".padEnd(hatLen, "^");
+ } else {
+ str += "\n at " + loc;
+ }
+ }
+ return str;
+ }
+
+ static buildMessage(expected, found) {
+ function hex(ch) {
+ return ch.codePointAt(0).toString(16).toUpperCase();
+ }
+
+ const nonPrintable = Object.prototype.hasOwnProperty.call(RegExp.prototype, "unicode")
+ ? new RegExp("[\\p{C}\\p{Mn}\\p{Mc}]", "gu")
+ : null;
+ function unicodeEscape(s) {
+ if (nonPrintable) {
+ return s.replace(nonPrintable, ch => "\\u{" + hex(ch) + "}");
+ }
+ return s;
+ }
+
+ function literalEscape(s) {
+ return unicodeEscape(s
+ .replace(/\\/g, "\\\\")
+ .replace(/"/g, "\\\"")
+ .replace(/\0/g, "\\0")
+ .replace(/\t/g, "\\t")
+ .replace(/\n/g, "\\n")
+ .replace(/\r/g, "\\r")
+ .replace(/[\x00-\x0F]/g, ch => "\\x0" + hex(ch))
+ .replace(/[\x10-\x1F\x7F-\x9F]/g, ch => "\\x" + hex(ch)));
+ }
+
+ function classEscape(s) {
+ return unicodeEscape(s
+ .replace(/\\/g, "\\\\")
+ .replace(/\]/g, "\\]")
+ .replace(/\^/g, "\\^")
+ .replace(/-/g, "\\-")
+ .replace(/\0/g, "\\0")
+ .replace(/\t/g, "\\t")
+ .replace(/\n/g, "\\n")
+ .replace(/\r/g, "\\r")
+ .replace(/[\x00-\x0F]/g, ch => "\\x0" + hex(ch))
+ .replace(/[\x10-\x1F\x7F-\x9F]/g, ch => "\\x" + hex(ch)));
+ }
+
+ const DESCRIBE_EXPECTATION_FNS = {
+ literal(expectation) {
+ return "\"" + literalEscape(expectation.text) + "\"";
+ },
+
+ class(expectation) {
+ const escapedParts = expectation.parts.map(
+ part => (Array.isArray(part)
+ ? classEscape(part[0]) + "-" + classEscape(part[1])
+ : classEscape(part))
+ );
+
+ return "[" + (expectation.inverted ? "^" : "") + escapedParts.join("") + "]" + (expectation.unicode ? "u" : "");
+ },
+
+ any() {
+ return "any character";
+ },
+
+ end() {
+ return "end of input";
+ },
+
+ other(expectation) {
+ return expectation.description;
+ },
+ };
+
+ function describeExpectation(expectation) {
+ return DESCRIBE_EXPECTATION_FNS[expectation.type](expectation);
+ }
+
+ function describeExpected(expected) {
+ const descriptions = expected.map(describeExpectation);
+ descriptions.sort();
+
+ if (descriptions.length > 0) {
+ let j = 1;
+ for (let i = 1; i < descriptions.length; i++) {
+ if (descriptions[i - 1] !== descriptions[i]) {
+ descriptions[j] = descriptions[i];
+ j++;
+ }
+ }
+ descriptions.length = j;
+ }
+
+ switch (descriptions.length) {
+ case 1:
+ return descriptions[0];
+
+ case 2:
+ return descriptions[0] + " or " + descriptions[1];
+
+ default:
+ return descriptions.slice(0, -1).join(", ")
+ + ", or "
+ + descriptions[descriptions.length - 1];
+ }
+ }
+
+ function describeFound(found) {
+ return found ? "\"" + literalEscape(found) + "\"" : "end of input";
+ }
+
+ return "Expected " + describeExpected(expected) + " but " + describeFound(found) + " found.";
+ }
+}
+
+function peg$parse(input, options) {
+ options = options !== undefined ? options : {};
+
+ const peg$FAILED = {};
+ const peg$source = options.grammarSource;
+
+ const peg$startRuleFunctions = {
+ start: peg$parsestart,
+ };
+ let peg$startRuleFunction = peg$parsestart;
+
+ const peg$c0 = "[";
+ const peg$c1 = "]";
+ const peg$c2 = "/";
+ const peg$c3 = "after";
+ const peg$c4 = "ms";
+ const peg$c5 = "s";
+ const peg$c6 = ";";
+ const peg$c7 = "=";
+ const peg$c8 = "(";
+ const peg$c9 = ")";
+ const peg$c10 = "true";
+ const peg$c11 = "false";
+ const peg$c12 = "^";
+
+ const peg$r0 = /^[a-zA-Z0-9]/;
+ const peg$r1 = /^[0-9]/;
+ const peg$r2 = /^[ \t\n\r]/;
+ const peg$r3 = /^[+\-]/;
+ const peg$r4 = /^[*\/]/;
+
+ const peg$e0 = peg$literalExpectation("[", false);
+ const peg$e1 = peg$literalExpectation("]", false);
+ const peg$e2 = peg$literalExpectation("/", false);
+ const peg$e3 = peg$literalExpectation("after", false);
+ const peg$e4 = peg$literalExpectation("ms", false);
+ const peg$e5 = peg$literalExpectation("s", false);
+ const peg$e6 = peg$literalExpectation(";", false);
+ const peg$e7 = peg$literalExpectation("=", false);
+ const peg$e8 = peg$classExpectation([["a", "z"], ["A", "Z"], ["0", "9"]], false, false, false);
+ const peg$e9 = peg$classExpectation([["0", "9"]], false, false, false);
+ const peg$e10 = peg$classExpectation([" ", "\t", "\n", "\r"], false, false, false);
+ const peg$e11 = peg$classExpectation(["+", "-"], false, false, false);
+ const peg$e12 = peg$classExpectation(["*", "/"], false, false, false);
+ const peg$e13 = peg$literalExpectation("(", false);
+ const peg$e14 = peg$literalExpectation(")", false);
+ const peg$e15 = peg$literalExpectation("true", false);
+ const peg$e16 = peg$literalExpectation("false", false);
+ const peg$e17 = peg$literalExpectation("^", false);
+
+ function peg$f0(trigger, guard, actions) {
+ return {
+ trigger,
+ guard: guard ? guard[2] : {kind: "literal", value: true},
+ actions: actions ? actions[2] : [],
+ };
+ }
+ function peg$f1(event) {
+ return {kind: "event", event};
+ }
+ function peg$f2(dur) {
+ return {kind: "after", durationMs: dur};
+ }
+ function peg$f3(num, u) {
+ return num * (u === "s" ? 1000 : 1);
+ }
+ function peg$f4() {
+ return text();
+ }
+ function peg$f5(head, tail) {
+ return [head, ...tail.map(t => t[3])];
+ }
+ function peg$f6(lhs, rhs) {
+ return {kind: "assignment", lhs, rhs};
+ }
+ function peg$f7() {
+ return text();
+ }
+ function peg$f8() {
+ return parseInt(text());
+ }
+ function peg$f9(prod, rest) {
+ if (rest === null) {
+ return prod;
+ }
+ return {
+ kind:"binaryExpr",
+ operator: rest[0][1],
+ lhs: prod,
+ rhs: rest[1],
+ };
+ }
+ function peg$f10(atom, rest) {
+ if (rest === null) {
+ return atom;
+ }
+ return {
+ kind:"binaryExpr",
+ operator: rest[0][1],
+ lhs: atom,
+ rhs: rest[1],
+ };
+ }
+ function peg$f11(expr) {
+ return expr;
+ }
+ function peg$f12(value) {
+ return {kind: "literal", value}
+ }
+ function peg$f13(variable) {
+ return {kind: "ref", variable}
+ }
+ function peg$f14() {
+ return text() === "true";
+ }
+ function peg$f15(event) {
+ return {kind: "raise", event};
+ }
+ let peg$currPos = options.peg$currPos | 0;
+ let peg$savedPos = peg$currPos;
+ const peg$posDetailsCache = [{ line: 1, column: 1 }];
+ let peg$maxFailPos = peg$currPos;
+ let peg$maxFailExpected = options.peg$maxFailExpected || [];
+ let peg$silentFails = options.peg$silentFails | 0;
+
+ let peg$result;
+
+ if (options.startRule) {
+ if (!(options.startRule in peg$startRuleFunctions)) {
+ throw new Error("Can't start parsing from rule \"" + options.startRule + "\".");
+ }
+
+ peg$startRuleFunction = peg$startRuleFunctions[options.startRule];
+ }
+
+ function text() {
+ return input.substring(peg$savedPos, peg$currPos);
+ }
+
+ function offset() {
+ return peg$savedPos;
+ }
+
+ function range() {
+ return {
+ source: peg$source,
+ start: peg$savedPos,
+ end: peg$currPos,
+ };
+ }
+
+ function location() {
+ return peg$computeLocation(peg$savedPos, peg$currPos);
+ }
+
+ function expected(description, location) {
+ location = location !== undefined
+ ? location
+ : peg$computeLocation(peg$savedPos, peg$currPos);
+
+ throw peg$buildStructuredError(
+ [peg$otherExpectation(description)],
+ input.substring(peg$savedPos, peg$currPos),
+ location
+ );
+ }
+
+ function error(message, location) {
+ location = location !== undefined
+ ? location
+ : peg$computeLocation(peg$savedPos, peg$currPos);
+
+ throw peg$buildSimpleError(message, location);
+ }
+
+ function peg$getUnicode(pos = peg$currPos) {
+ const cp = input.codePointAt(pos);
+ if (cp === undefined) {
+ return "";
+ }
+ return String.fromCodePoint(cp);
+ }
+
+ function peg$literalExpectation(text, ignoreCase) {
+ return { type: "literal", text, ignoreCase };
+ }
+
+ function peg$classExpectation(parts, inverted, ignoreCase, unicode) {
+ return { type: "class", parts, inverted, ignoreCase, unicode };
+ }
+
+ function peg$anyExpectation() {
+ return { type: "any" };
+ }
+
+ function peg$endExpectation() {
+ return { type: "end" };
+ }
+
+ function peg$otherExpectation(description) {
+ return { type: "other", description };
+ }
+
+ function peg$computePosDetails(pos) {
+ let details = peg$posDetailsCache[pos];
+ let p;
+
+ if (details) {
+ return details;
+ } else {
+ if (pos >= peg$posDetailsCache.length) {
+ p = peg$posDetailsCache.length - 1;
+ } else {
+ p = pos;
+ while (!peg$posDetailsCache[--p]) {}
+ }
+
+ details = peg$posDetailsCache[p];
+ details = {
+ line: details.line,
+ column: details.column,
+ };
+
+ while (p < pos) {
+ if (input.charCodeAt(p) === 10) {
+ details.line++;
+ details.column = 1;
+ } else {
+ details.column++;
+ }
+
+ p++;
+ }
+
+ peg$posDetailsCache[pos] = details;
+
+ return details;
+ }
+ }
+
+ function peg$computeLocation(startPos, endPos, offset) {
+ const startPosDetails = peg$computePosDetails(startPos);
+ const endPosDetails = peg$computePosDetails(endPos);
+
+ const res = {
+ source: peg$source,
+ start: {
+ offset: startPos,
+ line: startPosDetails.line,
+ column: startPosDetails.column,
+ },
+ end: {
+ offset: endPos,
+ line: endPosDetails.line,
+ column: endPosDetails.column,
+ },
+ };
+ if (offset && peg$source && (typeof peg$source.offset === "function")) {
+ res.start = peg$source.offset(res.start);
+ res.end = peg$source.offset(res.end);
+ }
+ return res;
+ }
+
+ function peg$fail(expected) {
+ if (peg$currPos < peg$maxFailPos) { return; }
+
+ if (peg$currPos > peg$maxFailPos) {
+ peg$maxFailPos = peg$currPos;
+ peg$maxFailExpected = [];
+ }
+
+ peg$maxFailExpected.push(expected);
+ }
+
+ function peg$buildSimpleError(message, location) {
+ return new peg$SyntaxError(message, null, null, location);
+ }
+
+ function peg$buildStructuredError(expected, found, location) {
+ return new peg$SyntaxError(
+ peg$SyntaxError.buildMessage(expected, found),
+ expected,
+ found,
+ location
+ );
+ }
+
+ function peg$parsestart() {
+ let s0, s1, s2, s3, s4, s5, s6, s7, s8, s9;
+
+ s0 = peg$currPos;
+ s1 = peg$parse_();
+ s2 = peg$parsetrigger();
+ if (s2 !== peg$FAILED) {
+ s3 = peg$parse_();
+ s4 = peg$currPos;
+ if (input.charCodeAt(peg$currPos) === 91) {
+ s5 = peg$c0;
+ peg$currPos++;
+ } else {
+ s5 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e0); }
+ }
+ if (s5 !== peg$FAILED) {
+ s6 = peg$parse_();
+ s7 = peg$parsesum();
+ if (s7 !== peg$FAILED) {
+ s8 = peg$parse_();
+ if (input.charCodeAt(peg$currPos) === 93) {
+ s9 = peg$c1;
+ peg$currPos++;
+ } else {
+ s9 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e1); }
+ }
+ if (s9 !== peg$FAILED) {
+ s5 = [s5, s6, s7, s8, s9];
+ s4 = s5;
+ } else {
+ peg$currPos = s4;
+ s4 = peg$FAILED;
+ }
+ } else {
+ peg$currPos = s4;
+ s4 = peg$FAILED;
+ }
+ } else {
+ peg$currPos = s4;
+ s4 = peg$FAILED;
+ }
+ if (s4 === peg$FAILED) {
+ s4 = null;
+ }
+ s5 = peg$parse_();
+ s6 = peg$currPos;
+ if (input.charCodeAt(peg$currPos) === 47) {
+ s7 = peg$c2;
+ peg$currPos++;
+ } else {
+ s7 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e2); }
+ }
+ if (s7 !== peg$FAILED) {
+ s8 = peg$parse_();
+ s9 = peg$parseactions();
+ if (s9 !== peg$FAILED) {
+ s7 = [s7, s8, s9];
+ s6 = s7;
+ } else {
+ peg$currPos = s6;
+ s6 = peg$FAILED;
+ }
+ } else {
+ peg$currPos = s6;
+ s6 = peg$FAILED;
+ }
+ if (s6 === peg$FAILED) {
+ s6 = null;
+ }
+ s7 = peg$parse_();
+ peg$savedPos = s0;
+ s0 = peg$f0(s2, s4, s6);
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+
+ return s0;
+ }
+
+ function peg$parsetrigger() {
+ let s0;
+
+ s0 = peg$parseafterTrigger();
+ if (s0 === peg$FAILED) {
+ s0 = peg$parseeventTrigger();
+ }
+
+ return s0;
+ }
+
+ function peg$parseeventTrigger() {
+ let s0, s1;
+
+ s0 = peg$currPos;
+ s1 = peg$parseidentifier();
+ if (s1 !== peg$FAILED) {
+ peg$savedPos = s0;
+ s1 = peg$f1(s1);
+ }
+ s0 = s1;
+
+ return s0;
+ }
+
+ function peg$parseafterTrigger() {
+ let s0, s1, s2, s3;
+
+ s0 = peg$currPos;
+ if (input.substr(peg$currPos, 5) === peg$c3) {
+ s1 = peg$c3;
+ peg$currPos += 5;
+ } else {
+ s1 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e3); }
+ }
+ if (s1 !== peg$FAILED) {
+ s2 = peg$parse_();
+ s3 = peg$parsedurationMs();
+ if (s3 !== peg$FAILED) {
+ peg$savedPos = s0;
+ s0 = peg$f2(s3);
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+
+ return s0;
+ }
+
+ function peg$parsedurationMs() {
+ let s0, s1, s2, s3;
+
+ s0 = peg$currPos;
+ s1 = peg$parsenumber();
+ if (s1 !== peg$FAILED) {
+ s2 = peg$parse_();
+ s3 = peg$parsetimeUnit();
+ if (s3 !== peg$FAILED) {
+ peg$savedPos = s0;
+ s0 = peg$f3(s1, s3);
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+
+ return s0;
+ }
+
+ function peg$parsetimeUnit() {
+ let s0, s1;
+
+ if (input.substr(peg$currPos, 2) === peg$c4) {
+ s0 = peg$c4;
+ peg$currPos += 2;
+ } else {
+ s0 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e4); }
+ }
+ if (s0 === peg$FAILED) {
+ s0 = peg$currPos;
+ if (input.charCodeAt(peg$currPos) === 115) {
+ s1 = peg$c5;
+ peg$currPos++;
+ } else {
+ s1 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e5); }
+ }
+ if (s1 !== peg$FAILED) {
+ peg$savedPos = s0;
+ s1 = peg$f4();
+ }
+ s0 = s1;
+ }
+
+ return s0;
+ }
+
+ function peg$parseactions() {
+ let s0, s1, s2, s3, s4, s5, s6, s7;
+
+ s0 = peg$currPos;
+ s1 = peg$parseaction();
+ if (s1 !== peg$FAILED) {
+ s2 = [];
+ s3 = peg$currPos;
+ s4 = peg$parse_();
+ if (input.charCodeAt(peg$currPos) === 59) {
+ s5 = peg$c6;
+ peg$currPos++;
+ } else {
+ s5 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e6); }
+ }
+ if (s5 !== peg$FAILED) {
+ s6 = peg$parse_();
+ s7 = peg$parseaction();
+ if (s7 !== peg$FAILED) {
+ s4 = [s4, s5, s6, s7];
+ s3 = s4;
+ } else {
+ peg$currPos = s3;
+ s3 = peg$FAILED;
+ }
+ } else {
+ peg$currPos = s3;
+ s3 = peg$FAILED;
+ }
+ while (s3 !== peg$FAILED) {
+ s2.push(s3);
+ s3 = peg$currPos;
+ s4 = peg$parse_();
+ if (input.charCodeAt(peg$currPos) === 59) {
+ s5 = peg$c6;
+ peg$currPos++;
+ } else {
+ s5 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e6); }
+ }
+ if (s5 !== peg$FAILED) {
+ s6 = peg$parse_();
+ s7 = peg$parseaction();
+ if (s7 !== peg$FAILED) {
+ s4 = [s4, s5, s6, s7];
+ s3 = s4;
+ } else {
+ peg$currPos = s3;
+ s3 = peg$FAILED;
+ }
+ } else {
+ peg$currPos = s3;
+ s3 = peg$FAILED;
+ }
+ }
+ s3 = peg$parse_();
+ if (input.charCodeAt(peg$currPos) === 59) {
+ s4 = peg$c6;
+ peg$currPos++;
+ } else {
+ s4 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e6); }
+ }
+ if (s4 === peg$FAILED) {
+ s4 = null;
+ }
+ peg$savedPos = s0;
+ s0 = peg$f5(s1, s2);
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+
+ return s0;
+ }
+
+ function peg$parseaction() {
+ let s0;
+
+ s0 = peg$parseassignment();
+ if (s0 === peg$FAILED) {
+ s0 = peg$parseraise();
+ }
+
+ return s0;
+ }
+
+ function peg$parseassignment() {
+ let s0, s1, s2, s3, s4, s5;
+
+ s0 = peg$currPos;
+ s1 = peg$parseidentifier();
+ if (s1 !== peg$FAILED) {
+ s2 = peg$parse_();
+ if (input.charCodeAt(peg$currPos) === 61) {
+ s3 = peg$c7;
+ peg$currPos++;
+ } else {
+ s3 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e7); }
+ }
+ if (s3 !== peg$FAILED) {
+ s4 = peg$parse_();
+ s5 = peg$parsesum();
+ if (s5 !== peg$FAILED) {
+ peg$savedPos = s0;
+ s0 = peg$f6(s1, s5);
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+
+ return s0;
+ }
+
+ function peg$parseidentifier() {
+ let s0, s1, s2;
+
+ s0 = peg$currPos;
+ s1 = [];
+ s2 = input.charAt(peg$currPos);
+ if (peg$r0.test(s2)) {
+ peg$currPos++;
+ } else {
+ s2 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e8); }
+ }
+ if (s2 !== peg$FAILED) {
+ while (s2 !== peg$FAILED) {
+ s1.push(s2);
+ s2 = input.charAt(peg$currPos);
+ if (peg$r0.test(s2)) {
+ peg$currPos++;
+ } else {
+ s2 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e8); }
+ }
+ }
+ } else {
+ s1 = peg$FAILED;
+ }
+ if (s1 !== peg$FAILED) {
+ peg$savedPos = s0;
+ s1 = peg$f7();
+ }
+ s0 = s1;
+
+ return s0;
+ }
+
+ function peg$parsenumber() {
+ let s0, s1, s2;
+
+ s0 = peg$currPos;
+ s1 = [];
+ s2 = input.charAt(peg$currPos);
+ if (peg$r1.test(s2)) {
+ peg$currPos++;
+ } else {
+ s2 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e9); }
+ }
+ if (s2 !== peg$FAILED) {
+ while (s2 !== peg$FAILED) {
+ s1.push(s2);
+ s2 = input.charAt(peg$currPos);
+ if (peg$r1.test(s2)) {
+ peg$currPos++;
+ } else {
+ s2 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e9); }
+ }
+ }
+ } else {
+ s1 = peg$FAILED;
+ }
+ if (s1 !== peg$FAILED) {
+ peg$savedPos = s0;
+ s1 = peg$f8();
+ }
+ s0 = s1;
+
+ return s0;
+ }
+
+ function peg$parse_() {
+ let s0, s1;
+
+ peg$silentFails++;
+ s0 = [];
+ s1 = input.charAt(peg$currPos);
+ if (peg$r2.test(s1)) {
+ peg$currPos++;
+ } else {
+ s1 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e10); }
+ }
+ while (s1 !== peg$FAILED) {
+ s0.push(s1);
+ s1 = input.charAt(peg$currPos);
+ if (peg$r2.test(s1)) {
+ peg$currPos++;
+ } else {
+ s1 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e10); }
+ }
+ }
+ peg$silentFails--;
+
+ return s0;
+ }
+
+ function peg$parsesum() {
+ let s0, s1, s2, s3, s4, s5, s6;
+
+ s0 = peg$currPos;
+ s1 = peg$parseproduct();
+ if (s1 !== peg$FAILED) {
+ s2 = peg$currPos;
+ s3 = peg$currPos;
+ s4 = peg$parse_();
+ s5 = input.charAt(peg$currPos);
+ if (peg$r3.test(s5)) {
+ peg$currPos++;
+ } else {
+ s5 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e11); }
+ }
+ if (s5 !== peg$FAILED) {
+ s6 = peg$parse_();
+ s4 = [s4, s5, s6];
+ s3 = s4;
+ } else {
+ peg$currPos = s3;
+ s3 = peg$FAILED;
+ }
+ if (s3 !== peg$FAILED) {
+ s4 = peg$parsesum();
+ if (s4 !== peg$FAILED) {
+ s3 = [s3, s4];
+ s2 = s3;
+ } else {
+ peg$currPos = s2;
+ s2 = peg$FAILED;
+ }
+ } else {
+ peg$currPos = s2;
+ s2 = peg$FAILED;
+ }
+ if (s2 === peg$FAILED) {
+ s2 = null;
+ }
+ peg$savedPos = s0;
+ s0 = peg$f9(s1, s2);
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+
+ return s0;
+ }
+
+ function peg$parseproduct() {
+ let s0, s1, s2, s3, s4, s5, s6;
+
+ s0 = peg$currPos;
+ s1 = peg$parseatom();
+ if (s1 !== peg$FAILED) {
+ s2 = peg$currPos;
+ s3 = peg$currPos;
+ s4 = peg$parse_();
+ s5 = input.charAt(peg$currPos);
+ if (peg$r4.test(s5)) {
+ peg$currPos++;
+ } else {
+ s5 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e12); }
+ }
+ if (s5 !== peg$FAILED) {
+ s6 = peg$parse_();
+ s4 = [s4, s5, s6];
+ s3 = s4;
+ } else {
+ peg$currPos = s3;
+ s3 = peg$FAILED;
+ }
+ if (s3 !== peg$FAILED) {
+ s4 = peg$parseproduct();
+ if (s4 !== peg$FAILED) {
+ s3 = [s3, s4];
+ s2 = s3;
+ } else {
+ peg$currPos = s2;
+ s2 = peg$FAILED;
+ }
+ } else {
+ peg$currPos = s2;
+ s2 = peg$FAILED;
+ }
+ if (s2 === peg$FAILED) {
+ s2 = null;
+ }
+ peg$savedPos = s0;
+ s0 = peg$f10(s1, s2);
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+
+ return s0;
+ }
+
+ function peg$parseatom() {
+ let s0;
+
+ s0 = peg$parsenested();
+ if (s0 === peg$FAILED) {
+ s0 = peg$parseliteral();
+ if (s0 === peg$FAILED) {
+ s0 = peg$parseref();
+ }
+ }
+
+ return s0;
+ }
+
+ function peg$parsenested() {
+ let s0, s1, s2, s3, s4, s5;
+
+ s0 = peg$currPos;
+ if (input.charCodeAt(peg$currPos) === 40) {
+ s1 = peg$c8;
+ peg$currPos++;
+ } else {
+ s1 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e13); }
+ }
+ if (s1 !== peg$FAILED) {
+ s2 = peg$parse_();
+ s3 = peg$parsesum();
+ if (s3 !== peg$FAILED) {
+ s4 = peg$parse_();
+ if (input.charCodeAt(peg$currPos) === 41) {
+ s5 = peg$c9;
+ peg$currPos++;
+ } else {
+ s5 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e14); }
+ }
+ if (s5 !== peg$FAILED) {
+ peg$savedPos = s0;
+ s0 = peg$f11(s3);
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+
+ return s0;
+ }
+
+ function peg$parseliteral() {
+ let s0, s1;
+
+ s0 = peg$currPos;
+ s1 = peg$parsenumber();
+ if (s1 === peg$FAILED) {
+ s1 = peg$parseboolean();
+ }
+ if (s1 !== peg$FAILED) {
+ peg$savedPos = s0;
+ s1 = peg$f12(s1);
+ }
+ s0 = s1;
+
+ return s0;
+ }
+
+ function peg$parseref() {
+ let s0, s1;
+
+ s0 = peg$currPos;
+ s1 = peg$parseidentifier();
+ if (s1 !== peg$FAILED) {
+ peg$savedPos = s0;
+ s1 = peg$f13(s1);
+ }
+ s0 = s1;
+
+ return s0;
+ }
+
+ function peg$parseboolean() {
+ let s0, s1;
+
+ s0 = peg$currPos;
+ if (input.substr(peg$currPos, 4) === peg$c10) {
+ s1 = peg$c10;
+ peg$currPos += 4;
+ } else {
+ s1 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e15); }
+ }
+ if (s1 === peg$FAILED) {
+ if (input.substr(peg$currPos, 5) === peg$c11) {
+ s1 = peg$c11;
+ peg$currPos += 5;
+ } else {
+ s1 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e16); }
+ }
+ }
+ if (s1 !== peg$FAILED) {
+ peg$savedPos = s0;
+ s1 = peg$f14();
+ }
+ s0 = s1;
+
+ return s0;
+ }
+
+ function peg$parseraise() {
+ let s0, s1, s2, s3;
+
+ s0 = peg$currPos;
+ if (input.charCodeAt(peg$currPos) === 94) {
+ s1 = peg$c12;
+ peg$currPos++;
+ } else {
+ s1 = peg$FAILED;
+ if (peg$silentFails === 0) { peg$fail(peg$e17); }
+ }
+ if (s1 !== peg$FAILED) {
+ s2 = peg$parse_();
+ s3 = peg$parseidentifier();
+ if (s3 !== peg$FAILED) {
+ peg$savedPos = s0;
+ s0 = peg$f15(s3);
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+ } else {
+ peg$currPos = s0;
+ s0 = peg$FAILED;
+ }
+
+ return s0;
+ }
+
+ peg$result = peg$startRuleFunction();
+
+ const peg$success = (peg$result !== peg$FAILED && peg$currPos === input.length);
+ function peg$throw() {
+ if (peg$result !== peg$FAILED && peg$currPos < input.length) {
+ peg$fail(peg$endExpectation());
+ }
+
+ throw peg$buildStructuredError(
+ peg$maxFailExpected,
+ peg$maxFailPos < input.length ? peg$getUnicode(peg$maxFailPos) : null,
+ peg$maxFailPos < input.length
+ ? peg$computeLocation(peg$maxFailPos, peg$maxFailPos + 1)
+ : peg$computeLocation(peg$maxFailPos, peg$maxFailPos)
+ );
+ }
+ if (options.peg$library) {
+ return /** @type {any} */ ({
+ peg$result,
+ peg$currPos,
+ peg$FAILED,
+ peg$maxFailExpected,
+ peg$maxFailPos,
+ peg$success,
+ peg$throw: peg$success ? undefined : peg$throw,
+ });
+ }
+ if (peg$success) {
+ return peg$result;
+ } else {
+ peg$throw();
+ }
+}
+
+const peg$allowedStartRules = [
+ "start"
+];
+
+export {
+ peg$allowedStartRules as StartRules,
+ peg$SyntaxError as SyntaxError,
+ peg$parse as parse
+};
diff --git a/src/VisualEditor/parser.ts b/src/VisualEditor/parser.ts
index 7fd221a..cd003a2 100644
--- a/src/VisualEditor/parser.ts
+++ b/src/VisualEditor/parser.ts
@@ -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();
+ // 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();
+ // step 2: figure out transitions
- const errorShapes: [string, string][] = [];
+ const transitions = new Map();
+ const uid2Transition = new Map();
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,
diff --git a/src/VisualEditor/transition_label.grammar b/src/VisualEditor/transition_label.grammar
new file mode 100644
index 0000000..b2f723c
--- /dev/null
+++ b/src/VisualEditor/transition_label.grammar
@@ -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};
+}
diff --git a/src/index.css b/src/index.css
index b67b04d..cc58cac 100644
--- a/src/index.css
+++ b/src/index.css
@@ -1,8 +1,9 @@
html, body {
margin: 0;
height: 100%;
+ font-family: Roboto, sans-serif;
}
div#root {
height: 100%;
-}
\ No newline at end of file
+}