store app state in URL hash
This commit is contained in:
parent
41f34ab65e
commit
9dd72484fa
10 changed files with 319 additions and 125 deletions
1
.npmrc
Normal file
1
.npmrc
Normal file
|
|
@ -0,0 +1 @@
|
|||
@jsr:registry=https://npm.jsr.io
|
||||
3
bun.lock
3
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=="],
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
"start": "NODE_ENV=production bun src/index.tsx"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nick/lz4": "npm:@jsr/nick__lz4",
|
||||
"react": "^19",
|
||||
"react-dom": "^19"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -136,3 +136,11 @@ text.error, tspan.error {
|
|||
fill: rgb(230,0,0);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.errorHover {
|
||||
display: none;
|
||||
}
|
||||
|
||||
g:hover > .errorHover {
|
||||
display: inline;
|
||||
}
|
||||
|
|
@ -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<SVGSVGElement>(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<string,string>();
|
||||
const arrow2TextMap = new Map<string,string[]>();
|
||||
const text2RountangleMap = new Map<string, string>();
|
||||
const rountangle2TextMap = new Map<string, string[]>();
|
||||
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 = <><text {...commonProps}>
|
||||
{txt.text.slice(0, start.offset)}
|
||||
<tspan className="error" data-uid={txt.uid} data-parts="text">
|
||||
{txt.text.slice(start.offset, end.offset)}
|
||||
{start.offset === end.offset && <>_</>}
|
||||
</tspan>
|
||||
{txt.text.slice(end.offset)}
|
||||
</>;
|
||||
</text>
|
||||
<text className="error errorHover" y={20} textAnchor="middle">{err.message}</text></>;
|
||||
}
|
||||
else {
|
||||
markedText = <>{txt.text}</>;
|
||||
textNode = <text {...commonProps}>{txt.text}</text>;
|
||||
}
|
||||
return <text
|
||||
return <g
|
||||
key={txt.uid}
|
||||
className={
|
||||
(selection.find(s => 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}
|
||||
</text>;})}
|
||||
>{textNode}</g>;})}
|
||||
|
||||
{selectingState && <Selecting {...selectingState} />}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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()) {
|
||||
|
|
|
|||
|
|
@ -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(''),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue