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 +}