From 9dd72484fa29f6766aa3ab3f182c102a0df3d4be Mon Sep 17 00:00:00 2001 From: Joeri Exelmans Date: Tue, 7 Oct 2025 15:49:19 +0200 Subject: [PATCH] store app state in URL hash --- .npmrc | 1 + bun.lock | 3 + package.json | 1 + src/VisualEditor/VisualEditor.css | 8 + src/VisualEditor/VisualEditor.tsx | 74 ++++--- src/VisualEditor/ast.ts | 1 + src/VisualEditor/label_ast.ts | 16 +- src/VisualEditor/label_parser.js | 240 ++++++++++++++++------ src/VisualEditor/parser.ts | 70 ++++--- src/VisualEditor/transition_label.grammar | 30 ++- 10 files changed, 319 insertions(+), 125 deletions(-) create mode 100644 .npmrc diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..41583e3 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +@jsr:registry=https://npm.jsr.io diff --git a/bun.lock b/bun.lock index 1858091..53efbae 100644 --- a/bun.lock +++ b/bun.lock @@ -4,6 +4,7 @@ "": { "name": "bun-react-template", "dependencies": { + "@nick/lz4": "npm:@jsr/nick__lz4", "react": "^19", "react-dom": "^19", }, @@ -15,6 +16,8 @@ }, }, "packages": { + "@nick/lz4": ["@jsr/nick__lz4@0.3.4", "https://npm.jsr.io/~/11/@jsr/nick__lz4/0.3.4.tgz", {}, "sha512-ZNc+8lCMC8D/cIa9GrSxRcEQC/MyThBOXXlg6rhrvAWSUcKPODwvscsVA+v1UugiBzfJ2dvQIZ/j8484PMadkg=="], + "@types/bun": ["@types/bun@1.2.23", "", { "dependencies": { "bun-types": "1.2.23" } }, "sha512-le8ueOY5b6VKYf19xT3McVbXqLqmxzPXHsQT/q9JHgikJ2X22wyTW3g3ohz2ZMnp7dod6aduIiq8A14Xyimm0A=="], "@types/node": ["@types/node@24.6.2", "", { "dependencies": { "undici-types": "~7.13.0" } }, "sha512-d2L25Y4j+W3ZlNAeMKcy7yDsK425ibcAOO2t7aPTz6gNMH0z2GThtwENCDc0d/Pw9wgyRqE5Px1wkV7naz8ang=="], diff --git a/package.json b/package.json index e5c5353..b1ba707 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "start": "NODE_ENV=production bun src/index.tsx" }, "dependencies": { + "@nick/lz4": "npm:@jsr/nick__lz4", "react": "^19", "react-dom": "^19" }, diff --git a/src/VisualEditor/VisualEditor.css b/src/VisualEditor/VisualEditor.css index c275e26..a15f2a5 100644 --- a/src/VisualEditor/VisualEditor.css +++ b/src/VisualEditor/VisualEditor.css @@ -136,3 +136,11 @@ text.error, tspan.error { fill: rgb(230,0,0); font-weight: 600; } + +.errorHover { + display: none; +} + +g:hover > .errorHover { + display: inline; +} \ No newline at end of file diff --git a/src/VisualEditor/VisualEditor.tsx b/src/VisualEditor/VisualEditor.tsx index 7758798..bccd45e 100644 --- a/src/VisualEditor/VisualEditor.tsx +++ b/src/VisualEditor/VisualEditor.tsx @@ -8,6 +8,8 @@ import { VisualEditorState, Rountangle, emptyState, Arrow, ArrowPart, Rountangle import { parseStatechart } from "./parser"; import { CORNER_HELPER_OFFSET, CORNER_HELPER_RADIUS, MIN_ROUNTANGLE_SIZE, ROUNTANGLE_RADIUS } from "./parameters"; +import * as lz4 from "@nick/lz4"; + type DraggingState = { lastMousePos: Vec2D; @@ -112,10 +114,15 @@ export function VisualEditor() { const refSVG = useRef(null); useEffect(() => { - const recoveredState = JSON.parse(window.localStorage.getItem("state") || "null"); - if (recoveredState) { + const compressedState = window.location.hash.slice(1); + try { + const compressedBuffer = Uint8Array.fromBase64(compressedState); + const recoveredState = JSON.parse(new TextDecoder().decode(lz4.decompress(compressedBuffer))); setState(recoveredState); } + catch (e) { + console.error("could not recover state:", e); + } }, []); useEffect(() => { @@ -123,8 +130,10 @@ export function VisualEditor() { // 1) it's a hack - prevents us from writing the initial state to localstorage (before having recovered the state that was in localstorage) // 2) performance: only save when the user does nothing const timeout = setTimeout(() => { - window.localStorage.setItem("state", JSON.stringify(state)); - // console.log('saved to localStorage'); + const stateBuffer = new TextEncoder().encode(JSON.stringify(state)); + const compressedStateBuffer = lz4.compress(stateBuffer); + const compressedStateString = compressedStateBuffer.toBase64(); + window.location.hash = "#"+compressedStateString; const [statechart, errors] = parseStatechart(state); console.log('statechart: ', statechart, 'errors:', errors); @@ -402,6 +411,7 @@ export function VisualEditor() { const text2ArrowMap = new Map(); const arrow2TextMap = new Map(); const text2RountangleMap = new Map(); + const rountangle2TextMap = new Map(); for (const arrow of state.arrows) { const startSide = findNearestRountangleSide(arrow, "start", state.rountangles); const endSide = findNearestRountangleSide(arrow, "end", state.rountangles); @@ -433,6 +443,9 @@ export function VisualEditor() { const rountangle = findRountangle(text.topLeft, state.rountangles); if (rountangle) { text2RountangleMap.set(text.uid, rountangle.uid); + const texts = rountangle2TextMap.get(rountangle.uid) || []; + texts.push(text.uid); + rountangle2TextMap.set(rountangle.uid, texts); } } } @@ -449,13 +462,14 @@ export function VisualEditor() { if (startSide) sidesToHighlight[startSide.uid] = [...sidesToHighlight[startSide.uid]||[], startSide.part]; if (endSide) sidesToHighlight[endSide.uid] = [...sidesToHighlight[endSide.uid]||[], endSide.part]; } - const texts = arrow2TextMap.get(selected.uid); - if (texts) { - for (const textUid of texts) { - textsToHighlight[textUid] = true; - } + const texts = [ + ...(arrow2TextMap.get(selected.uid) || []), + ...(rountangle2TextMap.get(selected.uid) || []), + ]; + for (const textUid of texts) { + textsToHighlight[textUid] = true; } - const arrows = side2ArrowMap.get(selected.uid); + const arrows = side2ArrowMap.get(selected.uid) || []; if (arrows) { for (const [arrowPart, arrowUid] of arrows) { arrowsToHighlight[arrowUid] = true; @@ -504,7 +518,6 @@ export function VisualEditor() { {state.arrows.map(arrow => { const sides = arrow2SideMap.get(arrow.uid); - console.log(sides, arrow); let arc = "no" as ArcDirection; if (sides && sides[0]?.uid === sides[1]?.uid && sides[0].uid !== undefined) { arc = arcDirection(sides[0]?.part, sides[1]?.part); @@ -522,32 +535,33 @@ export function VisualEditor() { {state.texts.map(txt => { const err = errors.find(([uid]) => txt.uid === uid)?.[1]; - let markedText; + const commonProps = { + "data-uid": txt.uid, + "data-parts": "text", + textAnchor: "middle", + className: + (selection.find(s => s.uid === txt.uid)?.parts?.length ? "selected":"") + +(textsToHighlight.hasOwnProperty(txt.uid)?" highlight":""), + } + let textNode; if (err) { const {start,end} = err.location; - markedText = <> + textNode = <> {txt.text.slice(0, start.offset)} {txt.text.slice(start.offset, end.offset)} {start.offset === end.offset && <>_} {txt.text.slice(end.offset)} - ; + + {err.message}; } else { - markedText = <>{txt.text}; + textNode = {txt.text}; } - return s.uid === txt.uid)?.parts?.length ? "selected":"") - +(textsToHighlight.hasOwnProperty(txt.uid)?" highlight":"") - } - x={txt.topLeft.x} - y={txt.topLeft.y} - textAnchor="middle" - data-uid={txt.uid} - data-parts="text" + transform={`translate(${txt.topLeft.x} ${txt.topLeft.y})`} onDoubleClick={() => { const newText = prompt("", txt.text); if (newText) { @@ -566,10 +580,14 @@ export function VisualEditor() { }), })); } + else if (newText === "") { + setState(state => ({ + ...state, + texts: state.texts.filter(t => t.uid !== txt.uid), + })); + } }} - > - {markedText} - ;})} + >{textNode};})} {selectingState && } diff --git a/src/VisualEditor/ast.ts b/src/VisualEditor/ast.ts index 63612e4..ebe6798 100644 --- a/src/VisualEditor/ast.ts +++ b/src/VisualEditor/ast.ts @@ -3,6 +3,7 @@ import { TransitionLabel } from "./label_ast"; export type AbstractState = { uid: string; children: ConcreteState[]; + comments: [string, string][]; // array of tuple (text-uid, text-text) } export type AndState = { diff --git a/src/VisualEditor/label_ast.ts b/src/VisualEditor/label_ast.ts index 6bec803..aab5805 100644 --- a/src/VisualEditor/label_ast.ts +++ b/src/VisualEditor/label_ast.ts @@ -1,11 +1,18 @@ +export type ParsedText = TransitionLabel | Comment; + export type TransitionLabel = { + kind: "transitionLabel"; trigger: Trigger; guard: Expression; actions: Action[]; } +export type Comment = { + kind: "comment"; + text: string; +} -export type Trigger = EventTrigger | AfterTrigger; +export type Trigger = EventTrigger | AfterTrigger | EntryTrigger | ExitTrigger; export type EventTrigger = { kind: "event"; @@ -17,6 +24,13 @@ export type AfterTrigger = { durationMs: number; } +export type EntryTrigger = { + kind: "entry"; +} +export type ExitTrigger = { + kind: "exit"; +} + export type Action = Assignment | RaiseEvent; diff --git a/src/VisualEditor/label_parser.js b/src/VisualEditor/label_parser.js index a334518..5bd0d23 100644 --- a/src/VisualEditor/label_parser.js +++ b/src/VisualEditor/label_parser.js @@ -183,13 +183,15 @@ function peg$parse(input, options) { const peg$c16 = "true"; const peg$c17 = "false"; const peg$c18 = "^"; + const peg$c19 = "//"; + const peg$c20 = "\n"; 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$r5 = /^[*\/]/; + const peg$r2 = /^[<>]/; + const peg$r3 = /^[+\-]/; + const peg$r4 = /^[*\/]/; + const peg$r5 = /^[ \t\n\r]/; const peg$e0 = peg$literalExpectation("[", false); const peg$e1 = peg$literalExpectation("]", false); @@ -203,25 +205,29 @@ function peg$parse(input, options) { const peg$e9 = peg$literalExpectation("=", false); const peg$e10 = peg$classExpectation([["a", "z"], ["A", "Z"], ["0", "9"]], false, false, false); const peg$e11 = peg$classExpectation([["0", "9"]], false, false, false); - const peg$e12 = peg$classExpectation([" ", "\t", "\n", "\r"], false, false, false); - const peg$e13 = peg$literalExpectation("==", false); - const peg$e14 = peg$literalExpectation("!=", false); - const peg$e15 = peg$classExpectation(["<", ">"], false, false, false); - const peg$e16 = peg$literalExpectation("<=", false); - const peg$e17 = peg$literalExpectation(">=", false); - const peg$e18 = peg$classExpectation(["+", "-"], false, false, false); - const peg$e19 = peg$classExpectation(["*", "/"], false, false, false); - const peg$e20 = peg$literalExpectation("(", false); - const peg$e21 = peg$literalExpectation(")", false); - const peg$e22 = peg$literalExpectation("true", false); - const peg$e23 = peg$literalExpectation("false", false); - const peg$e24 = peg$literalExpectation("^", false); + const peg$e12 = peg$literalExpectation("==", false); + const peg$e13 = peg$literalExpectation("!=", false); + const peg$e14 = peg$classExpectation(["<", ">"], false, false, false); + const peg$e15 = peg$literalExpectation("<=", false); + const peg$e16 = peg$literalExpectation(">=", false); + const peg$e17 = peg$classExpectation(["+", "-"], false, false, false); + const peg$e18 = peg$classExpectation(["*", "/"], false, false, false); + const peg$e19 = peg$literalExpectation("(", false); + const peg$e20 = peg$literalExpectation(")", false); + const peg$e21 = peg$literalExpectation("true", false); + const peg$e22 = peg$literalExpectation("false", false); + const peg$e23 = peg$literalExpectation("^", false); + const peg$e24 = peg$classExpectation([" ", "\t", "\n", "\r"], false, false, false); + const peg$e25 = peg$literalExpectation("//", false); + const peg$e26 = peg$anyExpectation(); + const peg$e27 = peg$literalExpectation("\n", false); function peg$f0(trigger, guard, actions) { return { - trigger, - guard: guard ? guard[2] : {kind: "literal", value: true}, - actions: actions ? actions[2] : [], + kind: "transitionLabel", + trigger, + guard: guard ? guard[2] : {kind: "literal", value: true}, + actions: actions ? actions[2] : [], }; } function peg$f1(event) { @@ -302,6 +308,13 @@ function peg$parse(input, options) { function peg$f18(event) { return {kind: "raise", event}; } + function peg$f19() { return null; } + function peg$f20(text) { + return { + kind: "comment", + text: text.join(''), + }; + } let peg$currPos = options.peg$currPos | 0; let peg$savedPos = peg$currPos; const peg$posDetailsCache = [{ line: 1, column: 1 }]; @@ -473,6 +486,17 @@ function peg$parse(input, options) { } function peg$parsestart() { + let s0; + + s0 = peg$parsetlabel(); + if (s0 === peg$FAILED) { + s0 = peg$parsecomment(); + } + + return s0; + } + + function peg$parsetlabel() { let s0, s1, s2, s3, s4, s5, s6, s7, s8, s9; s0 = peg$currPos; @@ -901,33 +925,6 @@ function peg$parse(input, options) { 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$e12); } - } - 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$e12); } - } - } - peg$silentFails--; - - return s0; - } - function peg$parsecompare() { let s0, s1, s2, s3, s4, s5, s6; @@ -942,7 +939,7 @@ function peg$parse(input, options) { peg$currPos += 2; } else { s5 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e13); } + if (peg$silentFails === 0) { peg$fail(peg$e12); } } if (s5 === peg$FAILED) { if (input.substr(peg$currPos, 2) === peg$c11) { @@ -950,15 +947,15 @@ function peg$parse(input, options) { peg$currPos += 2; } else { s5 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e14); } + if (peg$silentFails === 0) { peg$fail(peg$e13); } } if (s5 === peg$FAILED) { s5 = input.charAt(peg$currPos); - if (peg$r3.test(s5)) { + if (peg$r2.test(s5)) { peg$currPos++; } else { s5 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e15); } + if (peg$silentFails === 0) { peg$fail(peg$e14); } } if (s5 === peg$FAILED) { if (input.substr(peg$currPos, 2) === peg$c12) { @@ -966,7 +963,7 @@ function peg$parse(input, options) { peg$currPos += 2; } else { s5 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e16); } + if (peg$silentFails === 0) { peg$fail(peg$e15); } } if (s5 === peg$FAILED) { if (input.substr(peg$currPos, 2) === peg$c13) { @@ -974,7 +971,7 @@ function peg$parse(input, options) { peg$currPos += 2; } else { s5 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e17); } + if (peg$silentFails === 0) { peg$fail(peg$e16); } } } } @@ -1024,11 +1021,11 @@ function peg$parse(input, options) { s3 = peg$currPos; s4 = peg$parse_(); s5 = input.charAt(peg$currPos); - if (peg$r4.test(s5)) { + if (peg$r3.test(s5)) { peg$currPos++; } else { s5 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e18); } + if (peg$silentFails === 0) { peg$fail(peg$e17); } } if (s5 !== peg$FAILED) { s6 = peg$parse_(); @@ -1074,11 +1071,11 @@ function peg$parse(input, options) { s3 = peg$currPos; s4 = peg$parse_(); s5 = input.charAt(peg$currPos); - if (peg$r5.test(s5)) { + if (peg$r4.test(s5)) { peg$currPos++; } else { s5 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e19); } + if (peg$silentFails === 0) { peg$fail(peg$e18); } } if (s5 !== peg$FAILED) { s6 = peg$parse_(); @@ -1137,7 +1134,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e20); } + if (peg$silentFails === 0) { peg$fail(peg$e19); } } if (s1 !== peg$FAILED) { s2 = peg$parse_(); @@ -1149,7 +1146,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s5 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e21); } + if (peg$silentFails === 0) { peg$fail(peg$e20); } } if (s5 !== peg$FAILED) { peg$savedPos = s0; @@ -1210,7 +1207,7 @@ function peg$parse(input, options) { peg$currPos += 4; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e22); } + if (peg$silentFails === 0) { peg$fail(peg$e21); } } if (s1 === peg$FAILED) { if (input.substr(peg$currPos, 5) === peg$c17) { @@ -1218,7 +1215,7 @@ function peg$parse(input, options) { peg$currPos += 5; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e23); } + if (peg$silentFails === 0) { peg$fail(peg$e22); } } } if (s1 !== peg$FAILED) { @@ -1239,7 +1236,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e24); } + if (peg$silentFails === 0) { peg$fail(peg$e23); } } if (s1 !== peg$FAILED) { s2 = peg$parse_(); @@ -1259,6 +1256,125 @@ function peg$parse(input, options) { return s0; } + function peg$parse_() { + let s0, s1, s2; + + peg$silentFails++; + s0 = peg$currPos; + s1 = []; + s2 = peg$parsecomment(); + if (s2 === peg$FAILED) { + s2 = input.charAt(peg$currPos); + if (peg$r5.test(s2)) { + peg$currPos++; + } else { + s2 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e24); } + } + } + while (s2 !== peg$FAILED) { + s1.push(s2); + s2 = peg$parsecomment(); + if (s2 === peg$FAILED) { + s2 = input.charAt(peg$currPos); + if (peg$r5.test(s2)) { + peg$currPos++; + } else { + s2 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e24); } + } + } + } + peg$savedPos = s0; + s1 = peg$f19(); + s0 = s1; + peg$silentFails--; + + return s0; + } + + function peg$parsecomment() { + let s0, s1, s2, s3, s4, s5, s6; + + s0 = peg$currPos; + if (input.substr(peg$currPos, 2) === peg$c19) { + s1 = peg$c19; + peg$currPos += 2; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e25); } + } + if (s1 !== peg$FAILED) { + s2 = peg$parse_(); + if (s2 !== peg$FAILED) { + s3 = []; + if (input.length > peg$currPos) { + s4 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s4 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e26); } + } + while (s4 !== peg$FAILED) { + s3.push(s4); + if (input.length > peg$currPos) { + s4 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s4 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e26); } + } + } + s4 = peg$parse_(); + if (s4 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 10) { + s5 = peg$c20; + peg$currPos++; + } else { + s5 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e27); } + } + if (s5 === peg$FAILED) { + s5 = peg$currPos; + peg$silentFails++; + if (input.length > peg$currPos) { + s6 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s6 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e26); } + } + peg$silentFails--; + if (s6 === peg$FAILED) { + s5 = undefined; + } else { + peg$currPos = s5; + s5 = peg$FAILED; + } + } + if (s5 !== peg$FAILED) { + peg$savedPos = s0; + s0 = peg$f20(s3); + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } 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); diff --git a/src/VisualEditor/parser.ts b/src/VisualEditor/parser.ts index 2b5aaf5..314e794 100644 --- a/src/VisualEditor/parser.ts +++ b/src/VisualEditor/parser.ts @@ -1,8 +1,8 @@ import { act } from "react"; import { ConcreteState, OrState, Statechart, Transition } from "./ast"; -import { findNearestArrow, findNearestRountangleSide, Rountangle, VisualEditorState } from "./editor_types"; +import { findNearestArrow, findNearestRountangleSide, findRountangle, Rountangle, VisualEditorState } from "./editor_types"; import { isEntirelyWithin, transformLine } from "./geometry"; -import { Action, Expression, TransitionLabel } from "./label_ast"; +import { Action, Expression, ParsedText, TransitionLabel } from "./label_ast"; import { parse as parseLabel, SyntaxError } from "./label_parser"; @@ -37,6 +37,7 @@ export function parseStatechart(state: VisualEditorState): [Statechart, [string, kind: rt.kind, uid: rt.uid, children: [], + comments: [], } if (state.kind === "or") { state.initial = []; @@ -122,18 +123,28 @@ 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); + let parsed: ParsedText; + try { + parsed = parseLabel(text.text); // may throw + } catch (e) { + if (e instanceof SyntaxError) { + errorShapes.push([text.uid, e]); + continue; + } + else { + throw e; + } + } + if (parsed.kind === "transitionLabel") { + const belongsToArrow = findNearestArrow(text.topLeft, state.arrows); + if (belongsToArrow) { + const belongsToTransition = uid2Transition.get(belongsToArrow.uid); + if (belongsToTransition) { + // parse as transition label + belongsToTransition.label.push(parsed); // collect events - if (transitionLabel.trigger.kind === "event") { - const {event} = transitionLabel.trigger; + if (parsed.trigger.kind === "event") { + const {event} = parsed.trigger; if (event.startsWith("_")) { internalEvents.add(event); } @@ -141,7 +152,7 @@ export function parseStatechart(state: VisualEditorState): [Statechart, [string, inputEvents.add(event); } } - for (const action of transitionLabel.actions) { + for (const action of parsed.actions) { if (action.kind === "raise") { const {event} = action; if (event.startsWith("_")) { @@ -154,22 +165,31 @@ export function parseStatechart(state: VisualEditorState): [Statechart, [string, } // collect variables variables = variables - .union(findVariables(transitionLabel.guard)); - for (const action of transitionLabel.actions) { + .union(findVariables(parsed.guard)); + for (const action of parsed.actions) { variables = variables.union(findVariablesAction(action)); } } - catch (e) { - if (e instanceof SyntaxError) { - belongsToTransition.label.push(null); - errorShapes.push([text.uid, e]); - } - else { - throw e; - } - } + continue; } } + // text does not belong to transition... + // so it belongs to a rountangle (a state) + const rountangle = findRountangle(text.topLeft, state.rountangles); + if (parsed.kind === "transitionLabel") { + // labels belonging to a rountangle (= a state) must by entry/exit actions + // if we cannot find a containing state, then it belong to the root + const state = rountangle ? uid2State.get(rountangle.uid)! : root; + if (parsed.trigger.kind !== "entry" && parsed.trigger.kind !== "exit") { + errorShapes.push([text.uid, { + message: "states can only have entry/exit triggers", + location: {start: {offset: 0}, end: {offset: text.text.length}}, + }]); + } + } + else if (parsed.kind === "comment") { + // just append comments to their respective states + } } for (const transition of uid2Transition.values()) { diff --git a/src/VisualEditor/transition_label.grammar b/src/VisualEditor/transition_label.grammar index 9198b9a..302b7bb 100644 --- a/src/VisualEditor/transition_label.grammar +++ b/src/VisualEditor/transition_label.grammar @@ -1,9 +1,12 @@ -start = _ trigger:trigger _ guard:("[" _ guard _ "]")? _ actions:("/" _ actions )? _ { - return { - trigger, - guard: guard ? guard[2] : {kind: "literal", value: true}, - actions: actions ? actions[2] : [], - }; +start = tlabel / comment + +tlabel = _ trigger:trigger _ guard:("[" _ guard _ "]")? _ actions:("/" _ actions )? _ { + return { + kind: "transitionLabel", + trigger, + guard: guard ? guard[2] : {kind: "literal", value: true}, + actions: actions ? actions[2] : [], + }; } trigger = afterTrigger / entryTrigger / exitTrigger / eventTrigger @@ -52,9 +55,6 @@ number = [0-9]+ { return parseInt(text()); } -_ "whitespace" - = [ \t\n\r]* - expr = compare compare = sum:sum rest:((_ ("==" / "!=" / "<" / ">" / "<=" / ">=") _) compare)? { @@ -114,3 +114,15 @@ boolean = ("true" / "false") { raise = "^" _ event:identifier { return {kind: "raise", event}; } + +_ "whitespace" + = (comment / [ \t\n\r])* + { return null; } + +comment = "//" _ text:.* _ ('\n' / !.) { + return { + kind: "comment", + text: text.join(''), + }; +} +