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",
|
"name": "bun-react-template",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@nick/lz4": "npm:@jsr/nick__lz4",
|
||||||
"react": "^19",
|
"react": "^19",
|
||||||
"react-dom": "^19",
|
"react-dom": "^19",
|
||||||
},
|
},
|
||||||
|
|
@ -15,6 +16,8 @@
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages": {
|
"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/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=="],
|
"@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"
|
"start": "NODE_ENV=production bun src/index.tsx"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@nick/lz4": "npm:@jsr/nick__lz4",
|
||||||
"react": "^19",
|
"react": "^19",
|
||||||
"react-dom": "^19"
|
"react-dom": "^19"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -136,3 +136,11 @@ text.error, tspan.error {
|
||||||
fill: rgb(230,0,0);
|
fill: rgb(230,0,0);
|
||||||
font-weight: 600;
|
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 { parseStatechart } from "./parser";
|
||||||
import { CORNER_HELPER_OFFSET, CORNER_HELPER_RADIUS, MIN_ROUNTANGLE_SIZE, ROUNTANGLE_RADIUS } from "./parameters";
|
import { CORNER_HELPER_OFFSET, CORNER_HELPER_RADIUS, MIN_ROUNTANGLE_SIZE, ROUNTANGLE_RADIUS } from "./parameters";
|
||||||
|
|
||||||
|
import * as lz4 from "@nick/lz4";
|
||||||
|
|
||||||
|
|
||||||
type DraggingState = {
|
type DraggingState = {
|
||||||
lastMousePos: Vec2D;
|
lastMousePos: Vec2D;
|
||||||
|
|
@ -112,10 +114,15 @@ export function VisualEditor() {
|
||||||
const refSVG = useRef<SVGSVGElement>(null);
|
const refSVG = useRef<SVGSVGElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const recoveredState = JSON.parse(window.localStorage.getItem("state") || "null");
|
const compressedState = window.location.hash.slice(1);
|
||||||
if (recoveredState) {
|
try {
|
||||||
|
const compressedBuffer = Uint8Array.fromBase64(compressedState);
|
||||||
|
const recoveredState = JSON.parse(new TextDecoder().decode(lz4.decompress(compressedBuffer)));
|
||||||
setState(recoveredState);
|
setState(recoveredState);
|
||||||
}
|
}
|
||||||
|
catch (e) {
|
||||||
|
console.error("could not recover state:", e);
|
||||||
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
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)
|
// 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
|
// 2) performance: only save when the user does nothing
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
window.localStorage.setItem("state", JSON.stringify(state));
|
const stateBuffer = new TextEncoder().encode(JSON.stringify(state));
|
||||||
// console.log('saved to localStorage');
|
const compressedStateBuffer = lz4.compress(stateBuffer);
|
||||||
|
const compressedStateString = compressedStateBuffer.toBase64();
|
||||||
|
window.location.hash = "#"+compressedStateString;
|
||||||
|
|
||||||
const [statechart, errors] = parseStatechart(state);
|
const [statechart, errors] = parseStatechart(state);
|
||||||
console.log('statechart: ', statechart, 'errors:', errors);
|
console.log('statechart: ', statechart, 'errors:', errors);
|
||||||
|
|
@ -402,6 +411,7 @@ export function VisualEditor() {
|
||||||
const text2ArrowMap = new Map<string,string>();
|
const text2ArrowMap = new Map<string,string>();
|
||||||
const arrow2TextMap = new Map<string,string[]>();
|
const arrow2TextMap = new Map<string,string[]>();
|
||||||
const text2RountangleMap = new Map<string, string>();
|
const text2RountangleMap = new Map<string, string>();
|
||||||
|
const rountangle2TextMap = new Map<string, string[]>();
|
||||||
for (const arrow of state.arrows) {
|
for (const arrow of state.arrows) {
|
||||||
const startSide = findNearestRountangleSide(arrow, "start", state.rountangles);
|
const startSide = findNearestRountangleSide(arrow, "start", state.rountangles);
|
||||||
const endSide = findNearestRountangleSide(arrow, "end", state.rountangles);
|
const endSide = findNearestRountangleSide(arrow, "end", state.rountangles);
|
||||||
|
|
@ -433,6 +443,9 @@ export function VisualEditor() {
|
||||||
const rountangle = findRountangle(text.topLeft, state.rountangles);
|
const rountangle = findRountangle(text.topLeft, state.rountangles);
|
||||||
if (rountangle) {
|
if (rountangle) {
|
||||||
text2RountangleMap.set(text.uid, rountangle.uid);
|
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 (startSide) sidesToHighlight[startSide.uid] = [...sidesToHighlight[startSide.uid]||[], startSide.part];
|
||||||
if (endSide) sidesToHighlight[endSide.uid] = [...sidesToHighlight[endSide.uid]||[], endSide.part];
|
if (endSide) sidesToHighlight[endSide.uid] = [...sidesToHighlight[endSide.uid]||[], endSide.part];
|
||||||
}
|
}
|
||||||
const texts = arrow2TextMap.get(selected.uid);
|
const texts = [
|
||||||
if (texts) {
|
...(arrow2TextMap.get(selected.uid) || []),
|
||||||
for (const textUid of texts) {
|
...(rountangle2TextMap.get(selected.uid) || []),
|
||||||
textsToHighlight[textUid] = true;
|
];
|
||||||
}
|
for (const textUid of texts) {
|
||||||
|
textsToHighlight[textUid] = true;
|
||||||
}
|
}
|
||||||
const arrows = side2ArrowMap.get(selected.uid);
|
const arrows = side2ArrowMap.get(selected.uid) || [];
|
||||||
if (arrows) {
|
if (arrows) {
|
||||||
for (const [arrowPart, arrowUid] of arrows) {
|
for (const [arrowPart, arrowUid] of arrows) {
|
||||||
arrowsToHighlight[arrowUid] = true;
|
arrowsToHighlight[arrowUid] = true;
|
||||||
|
|
@ -504,7 +518,6 @@ export function VisualEditor() {
|
||||||
|
|
||||||
{state.arrows.map(arrow => {
|
{state.arrows.map(arrow => {
|
||||||
const sides = arrow2SideMap.get(arrow.uid);
|
const sides = arrow2SideMap.get(arrow.uid);
|
||||||
console.log(sides, arrow);
|
|
||||||
let arc = "no" as ArcDirection;
|
let arc = "no" as ArcDirection;
|
||||||
if (sides && sides[0]?.uid === sides[1]?.uid && sides[0].uid !== undefined) {
|
if (sides && sides[0]?.uid === sides[1]?.uid && sides[0].uid !== undefined) {
|
||||||
arc = arcDirection(sides[0]?.part, sides[1]?.part);
|
arc = arcDirection(sides[0]?.part, sides[1]?.part);
|
||||||
|
|
@ -522,32 +535,33 @@ export function VisualEditor() {
|
||||||
|
|
||||||
{state.texts.map(txt => {
|
{state.texts.map(txt => {
|
||||||
const err = errors.find(([uid]) => txt.uid === uid)?.[1];
|
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) {
|
if (err) {
|
||||||
const {start,end} = err.location;
|
const {start,end} = err.location;
|
||||||
markedText = <>
|
textNode = <><text {...commonProps}>
|
||||||
{txt.text.slice(0, start.offset)}
|
{txt.text.slice(0, start.offset)}
|
||||||
<tspan className="error" data-uid={txt.uid} data-parts="text">
|
<tspan className="error" data-uid={txt.uid} data-parts="text">
|
||||||
{txt.text.slice(start.offset, end.offset)}
|
{txt.text.slice(start.offset, end.offset)}
|
||||||
{start.offset === end.offset && <>_</>}
|
{start.offset === end.offset && <>_</>}
|
||||||
</tspan>
|
</tspan>
|
||||||
{txt.text.slice(end.offset)}
|
{txt.text.slice(end.offset)}
|
||||||
</>;
|
</text>
|
||||||
|
<text className="error errorHover" y={20} textAnchor="middle">{err.message}</text></>;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
markedText = <>{txt.text}</>;
|
textNode = <text {...commonProps}>{txt.text}</text>;
|
||||||
}
|
}
|
||||||
return <text
|
return <g
|
||||||
key={txt.uid}
|
key={txt.uid}
|
||||||
className={
|
transform={`translate(${txt.topLeft.x} ${txt.topLeft.y})`}
|
||||||
(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"
|
|
||||||
onDoubleClick={() => {
|
onDoubleClick={() => {
|
||||||
const newText = prompt("", txt.text);
|
const newText = prompt("", txt.text);
|
||||||
if (newText) {
|
if (newText) {
|
||||||
|
|
@ -566,10 +580,14 @@ export function VisualEditor() {
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
else if (newText === "") {
|
||||||
|
setState(state => ({
|
||||||
|
...state,
|
||||||
|
texts: state.texts.filter(t => t.uid !== txt.uid),
|
||||||
|
}));
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>{textNode}</g>;})}
|
||||||
{markedText}
|
|
||||||
</text>;})}
|
|
||||||
|
|
||||||
{selectingState && <Selecting {...selectingState} />}
|
{selectingState && <Selecting {...selectingState} />}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { TransitionLabel } from "./label_ast";
|
||||||
export type AbstractState = {
|
export type AbstractState = {
|
||||||
uid: string;
|
uid: string;
|
||||||
children: ConcreteState[];
|
children: ConcreteState[];
|
||||||
|
comments: [string, string][]; // array of tuple (text-uid, text-text)
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AndState = {
|
export type AndState = {
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,18 @@
|
||||||
|
export type ParsedText = TransitionLabel | Comment;
|
||||||
|
|
||||||
export type TransitionLabel = {
|
export type TransitionLabel = {
|
||||||
|
kind: "transitionLabel";
|
||||||
trigger: Trigger;
|
trigger: Trigger;
|
||||||
guard: Expression;
|
guard: Expression;
|
||||||
actions: Action[];
|
actions: Action[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type Comment = {
|
||||||
|
kind: "comment";
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
export type Trigger = EventTrigger | AfterTrigger;
|
export type Trigger = EventTrigger | AfterTrigger | EntryTrigger | ExitTrigger;
|
||||||
|
|
||||||
export type EventTrigger = {
|
export type EventTrigger = {
|
||||||
kind: "event";
|
kind: "event";
|
||||||
|
|
@ -17,6 +24,13 @@ export type AfterTrigger = {
|
||||||
durationMs: number;
|
durationMs: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type EntryTrigger = {
|
||||||
|
kind: "entry";
|
||||||
|
}
|
||||||
|
export type ExitTrigger = {
|
||||||
|
kind: "exit";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export type Action = Assignment | RaiseEvent;
|
export type Action = Assignment | RaiseEvent;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -183,13 +183,15 @@ function peg$parse(input, options) {
|
||||||
const peg$c16 = "true";
|
const peg$c16 = "true";
|
||||||
const peg$c17 = "false";
|
const peg$c17 = "false";
|
||||||
const peg$c18 = "^";
|
const peg$c18 = "^";
|
||||||
|
const peg$c19 = "//";
|
||||||
|
const peg$c20 = "\n";
|
||||||
|
|
||||||
const peg$r0 = /^[a-zA-Z0-9]/;
|
const peg$r0 = /^[a-zA-Z0-9]/;
|
||||||
const peg$r1 = /^[0-9]/;
|
const peg$r1 = /^[0-9]/;
|
||||||
const peg$r2 = /^[ \t\n\r]/;
|
const peg$r2 = /^[<>]/;
|
||||||
const peg$r3 = /^[<>]/;
|
const peg$r3 = /^[+\-]/;
|
||||||
const peg$r4 = /^[+\-]/;
|
const peg$r4 = /^[*\/]/;
|
||||||
const peg$r5 = /^[*\/]/;
|
const peg$r5 = /^[ \t\n\r]/;
|
||||||
|
|
||||||
const peg$e0 = peg$literalExpectation("[", false);
|
const peg$e0 = peg$literalExpectation("[", false);
|
||||||
const peg$e1 = 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$e9 = peg$literalExpectation("=", false);
|
||||||
const peg$e10 = peg$classExpectation([["a", "z"], ["A", "Z"], ["0", "9"]], false, false, 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$e11 = peg$classExpectation([["0", "9"]], false, false, false);
|
||||||
const peg$e12 = peg$classExpectation([" ", "\t", "\n", "\r"], false, false, false);
|
const peg$e12 = peg$literalExpectation("==", false);
|
||||||
const peg$e13 = peg$literalExpectation("==", false);
|
const peg$e13 = peg$literalExpectation("!=", false);
|
||||||
const peg$e14 = peg$literalExpectation("!=", false);
|
const peg$e14 = peg$classExpectation(["<", ">"], false, false, false);
|
||||||
const peg$e15 = peg$classExpectation(["<", ">"], false, false, false);
|
const peg$e15 = peg$literalExpectation("<=", false);
|
||||||
const peg$e16 = peg$literalExpectation("<=", false);
|
const peg$e16 = peg$literalExpectation(">=", false);
|
||||||
const peg$e17 = peg$literalExpectation(">=", false);
|
const peg$e17 = peg$classExpectation(["+", "-"], false, false, false);
|
||||||
const peg$e18 = peg$classExpectation(["+", "-"], false, false, false);
|
const peg$e18 = peg$classExpectation(["*", "/"], false, false, false);
|
||||||
const peg$e19 = peg$classExpectation(["*", "/"], false, false, false);
|
const peg$e19 = peg$literalExpectation("(", false);
|
||||||
const peg$e20 = peg$literalExpectation("(", false);
|
const peg$e20 = peg$literalExpectation(")", false);
|
||||||
const peg$e21 = peg$literalExpectation(")", false);
|
const peg$e21 = peg$literalExpectation("true", false);
|
||||||
const peg$e22 = peg$literalExpectation("true", false);
|
const peg$e22 = peg$literalExpectation("false", false);
|
||||||
const peg$e23 = peg$literalExpectation("false", false);
|
const peg$e23 = peg$literalExpectation("^", false);
|
||||||
const peg$e24 = 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) {
|
function peg$f0(trigger, guard, actions) {
|
||||||
return {
|
return {
|
||||||
trigger,
|
kind: "transitionLabel",
|
||||||
guard: guard ? guard[2] : {kind: "literal", value: true},
|
trigger,
|
||||||
actions: actions ? actions[2] : [],
|
guard: guard ? guard[2] : {kind: "literal", value: true},
|
||||||
|
actions: actions ? actions[2] : [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
function peg$f1(event) {
|
function peg$f1(event) {
|
||||||
|
|
@ -302,6 +308,13 @@ function peg$parse(input, options) {
|
||||||
function peg$f18(event) {
|
function peg$f18(event) {
|
||||||
return {kind: "raise", 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$currPos = options.peg$currPos | 0;
|
||||||
let peg$savedPos = peg$currPos;
|
let peg$savedPos = peg$currPos;
|
||||||
const peg$posDetailsCache = [{ line: 1, column: 1 }];
|
const peg$posDetailsCache = [{ line: 1, column: 1 }];
|
||||||
|
|
@ -473,6 +486,17 @@ function peg$parse(input, options) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function peg$parsestart() {
|
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;
|
let s0, s1, s2, s3, s4, s5, s6, s7, s8, s9;
|
||||||
|
|
||||||
s0 = peg$currPos;
|
s0 = peg$currPos;
|
||||||
|
|
@ -901,33 +925,6 @@ function peg$parse(input, options) {
|
||||||
return s0;
|
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() {
|
function peg$parsecompare() {
|
||||||
let s0, s1, s2, s3, s4, s5, s6;
|
let s0, s1, s2, s3, s4, s5, s6;
|
||||||
|
|
||||||
|
|
@ -942,7 +939,7 @@ function peg$parse(input, options) {
|
||||||
peg$currPos += 2;
|
peg$currPos += 2;
|
||||||
} else {
|
} else {
|
||||||
s5 = peg$FAILED;
|
s5 = peg$FAILED;
|
||||||
if (peg$silentFails === 0) { peg$fail(peg$e13); }
|
if (peg$silentFails === 0) { peg$fail(peg$e12); }
|
||||||
}
|
}
|
||||||
if (s5 === peg$FAILED) {
|
if (s5 === peg$FAILED) {
|
||||||
if (input.substr(peg$currPos, 2) === peg$c11) {
|
if (input.substr(peg$currPos, 2) === peg$c11) {
|
||||||
|
|
@ -950,15 +947,15 @@ function peg$parse(input, options) {
|
||||||
peg$currPos += 2;
|
peg$currPos += 2;
|
||||||
} else {
|
} else {
|
||||||
s5 = peg$FAILED;
|
s5 = peg$FAILED;
|
||||||
if (peg$silentFails === 0) { peg$fail(peg$e14); }
|
if (peg$silentFails === 0) { peg$fail(peg$e13); }
|
||||||
}
|
}
|
||||||
if (s5 === peg$FAILED) {
|
if (s5 === peg$FAILED) {
|
||||||
s5 = input.charAt(peg$currPos);
|
s5 = input.charAt(peg$currPos);
|
||||||
if (peg$r3.test(s5)) {
|
if (peg$r2.test(s5)) {
|
||||||
peg$currPos++;
|
peg$currPos++;
|
||||||
} else {
|
} else {
|
||||||
s5 = peg$FAILED;
|
s5 = peg$FAILED;
|
||||||
if (peg$silentFails === 0) { peg$fail(peg$e15); }
|
if (peg$silentFails === 0) { peg$fail(peg$e14); }
|
||||||
}
|
}
|
||||||
if (s5 === peg$FAILED) {
|
if (s5 === peg$FAILED) {
|
||||||
if (input.substr(peg$currPos, 2) === peg$c12) {
|
if (input.substr(peg$currPos, 2) === peg$c12) {
|
||||||
|
|
@ -966,7 +963,7 @@ function peg$parse(input, options) {
|
||||||
peg$currPos += 2;
|
peg$currPos += 2;
|
||||||
} else {
|
} else {
|
||||||
s5 = peg$FAILED;
|
s5 = peg$FAILED;
|
||||||
if (peg$silentFails === 0) { peg$fail(peg$e16); }
|
if (peg$silentFails === 0) { peg$fail(peg$e15); }
|
||||||
}
|
}
|
||||||
if (s5 === peg$FAILED) {
|
if (s5 === peg$FAILED) {
|
||||||
if (input.substr(peg$currPos, 2) === peg$c13) {
|
if (input.substr(peg$currPos, 2) === peg$c13) {
|
||||||
|
|
@ -974,7 +971,7 @@ function peg$parse(input, options) {
|
||||||
peg$currPos += 2;
|
peg$currPos += 2;
|
||||||
} else {
|
} else {
|
||||||
s5 = peg$FAILED;
|
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;
|
s3 = peg$currPos;
|
||||||
s4 = peg$parse_();
|
s4 = peg$parse_();
|
||||||
s5 = input.charAt(peg$currPos);
|
s5 = input.charAt(peg$currPos);
|
||||||
if (peg$r4.test(s5)) {
|
if (peg$r3.test(s5)) {
|
||||||
peg$currPos++;
|
peg$currPos++;
|
||||||
} else {
|
} else {
|
||||||
s5 = peg$FAILED;
|
s5 = peg$FAILED;
|
||||||
if (peg$silentFails === 0) { peg$fail(peg$e18); }
|
if (peg$silentFails === 0) { peg$fail(peg$e17); }
|
||||||
}
|
}
|
||||||
if (s5 !== peg$FAILED) {
|
if (s5 !== peg$FAILED) {
|
||||||
s6 = peg$parse_();
|
s6 = peg$parse_();
|
||||||
|
|
@ -1074,11 +1071,11 @@ function peg$parse(input, options) {
|
||||||
s3 = peg$currPos;
|
s3 = peg$currPos;
|
||||||
s4 = peg$parse_();
|
s4 = peg$parse_();
|
||||||
s5 = input.charAt(peg$currPos);
|
s5 = input.charAt(peg$currPos);
|
||||||
if (peg$r5.test(s5)) {
|
if (peg$r4.test(s5)) {
|
||||||
peg$currPos++;
|
peg$currPos++;
|
||||||
} else {
|
} else {
|
||||||
s5 = peg$FAILED;
|
s5 = peg$FAILED;
|
||||||
if (peg$silentFails === 0) { peg$fail(peg$e19); }
|
if (peg$silentFails === 0) { peg$fail(peg$e18); }
|
||||||
}
|
}
|
||||||
if (s5 !== peg$FAILED) {
|
if (s5 !== peg$FAILED) {
|
||||||
s6 = peg$parse_();
|
s6 = peg$parse_();
|
||||||
|
|
@ -1137,7 +1134,7 @@ function peg$parse(input, options) {
|
||||||
peg$currPos++;
|
peg$currPos++;
|
||||||
} else {
|
} else {
|
||||||
s1 = peg$FAILED;
|
s1 = peg$FAILED;
|
||||||
if (peg$silentFails === 0) { peg$fail(peg$e20); }
|
if (peg$silentFails === 0) { peg$fail(peg$e19); }
|
||||||
}
|
}
|
||||||
if (s1 !== peg$FAILED) {
|
if (s1 !== peg$FAILED) {
|
||||||
s2 = peg$parse_();
|
s2 = peg$parse_();
|
||||||
|
|
@ -1149,7 +1146,7 @@ function peg$parse(input, options) {
|
||||||
peg$currPos++;
|
peg$currPos++;
|
||||||
} else {
|
} else {
|
||||||
s5 = peg$FAILED;
|
s5 = peg$FAILED;
|
||||||
if (peg$silentFails === 0) { peg$fail(peg$e21); }
|
if (peg$silentFails === 0) { peg$fail(peg$e20); }
|
||||||
}
|
}
|
||||||
if (s5 !== peg$FAILED) {
|
if (s5 !== peg$FAILED) {
|
||||||
peg$savedPos = s0;
|
peg$savedPos = s0;
|
||||||
|
|
@ -1210,7 +1207,7 @@ function peg$parse(input, options) {
|
||||||
peg$currPos += 4;
|
peg$currPos += 4;
|
||||||
} else {
|
} else {
|
||||||
s1 = peg$FAILED;
|
s1 = peg$FAILED;
|
||||||
if (peg$silentFails === 0) { peg$fail(peg$e22); }
|
if (peg$silentFails === 0) { peg$fail(peg$e21); }
|
||||||
}
|
}
|
||||||
if (s1 === peg$FAILED) {
|
if (s1 === peg$FAILED) {
|
||||||
if (input.substr(peg$currPos, 5) === peg$c17) {
|
if (input.substr(peg$currPos, 5) === peg$c17) {
|
||||||
|
|
@ -1218,7 +1215,7 @@ function peg$parse(input, options) {
|
||||||
peg$currPos += 5;
|
peg$currPos += 5;
|
||||||
} else {
|
} else {
|
||||||
s1 = peg$FAILED;
|
s1 = peg$FAILED;
|
||||||
if (peg$silentFails === 0) { peg$fail(peg$e23); }
|
if (peg$silentFails === 0) { peg$fail(peg$e22); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (s1 !== peg$FAILED) {
|
if (s1 !== peg$FAILED) {
|
||||||
|
|
@ -1239,7 +1236,7 @@ function peg$parse(input, options) {
|
||||||
peg$currPos++;
|
peg$currPos++;
|
||||||
} else {
|
} else {
|
||||||
s1 = peg$FAILED;
|
s1 = peg$FAILED;
|
||||||
if (peg$silentFails === 0) { peg$fail(peg$e24); }
|
if (peg$silentFails === 0) { peg$fail(peg$e23); }
|
||||||
}
|
}
|
||||||
if (s1 !== peg$FAILED) {
|
if (s1 !== peg$FAILED) {
|
||||||
s2 = peg$parse_();
|
s2 = peg$parse_();
|
||||||
|
|
@ -1259,6 +1256,125 @@ function peg$parse(input, options) {
|
||||||
return s0;
|
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();
|
peg$result = peg$startRuleFunction();
|
||||||
|
|
||||||
const peg$success = (peg$result !== peg$FAILED && peg$currPos === input.length);
|
const peg$success = (peg$result !== peg$FAILED && peg$currPos === input.length);
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { act } from "react";
|
import { act } from "react";
|
||||||
import { ConcreteState, OrState, Statechart, Transition } from "./ast";
|
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 { 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";
|
import { parse as parseLabel, SyntaxError } from "./label_parser";
|
||||||
|
|
||||||
|
|
@ -37,6 +37,7 @@ export function parseStatechart(state: VisualEditorState): [Statechart, [string,
|
||||||
kind: rt.kind,
|
kind: rt.kind,
|
||||||
uid: rt.uid,
|
uid: rt.uid,
|
||||||
children: [],
|
children: [],
|
||||||
|
comments: [],
|
||||||
}
|
}
|
||||||
if (state.kind === "or") {
|
if (state.kind === "or") {
|
||||||
state.initial = [];
|
state.initial = [];
|
||||||
|
|
@ -122,18 +123,28 @@ export function parseStatechart(state: VisualEditorState): [Statechart, [string,
|
||||||
// step 3: figure out labels
|
// step 3: figure out labels
|
||||||
|
|
||||||
for (const text of state.texts) {
|
for (const text of state.texts) {
|
||||||
const belongsToArrow = findNearestArrow(text.topLeft, state.arrows);
|
let parsed: ParsedText;
|
||||||
if (belongsToArrow) {
|
try {
|
||||||
const belongsToTransition = uid2Transition.get(belongsToArrow.uid);
|
parsed = parseLabel(text.text); // may throw
|
||||||
if (belongsToTransition) {
|
} catch (e) {
|
||||||
// parse as transition label
|
if (e instanceof SyntaxError) {
|
||||||
let transitionLabel: TransitionLabel;
|
errorShapes.push([text.uid, e]);
|
||||||
try {
|
continue;
|
||||||
transitionLabel = parseLabel(text.text); // may throw
|
}
|
||||||
belongsToTransition.label.push(transitionLabel);
|
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
|
// collect events
|
||||||
if (transitionLabel.trigger.kind === "event") {
|
if (parsed.trigger.kind === "event") {
|
||||||
const {event} = transitionLabel.trigger;
|
const {event} = parsed.trigger;
|
||||||
if (event.startsWith("_")) {
|
if (event.startsWith("_")) {
|
||||||
internalEvents.add(event);
|
internalEvents.add(event);
|
||||||
}
|
}
|
||||||
|
|
@ -141,7 +152,7 @@ export function parseStatechart(state: VisualEditorState): [Statechart, [string,
|
||||||
inputEvents.add(event);
|
inputEvents.add(event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const action of transitionLabel.actions) {
|
for (const action of parsed.actions) {
|
||||||
if (action.kind === "raise") {
|
if (action.kind === "raise") {
|
||||||
const {event} = action;
|
const {event} = action;
|
||||||
if (event.startsWith("_")) {
|
if (event.startsWith("_")) {
|
||||||
|
|
@ -154,22 +165,31 @@ export function parseStatechart(state: VisualEditorState): [Statechart, [string,
|
||||||
}
|
}
|
||||||
// collect variables
|
// collect variables
|
||||||
variables = variables
|
variables = variables
|
||||||
.union(findVariables(transitionLabel.guard));
|
.union(findVariables(parsed.guard));
|
||||||
for (const action of transitionLabel.actions) {
|
for (const action of parsed.actions) {
|
||||||
variables = variables.union(findVariablesAction(action));
|
variables = variables.union(findVariablesAction(action));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (e) {
|
continue;
|
||||||
if (e instanceof SyntaxError) {
|
|
||||||
belongsToTransition.label.push(null);
|
|
||||||
errorShapes.push([text.uid, e]);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 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()) {
|
for (const transition of uid2Transition.values()) {
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,12 @@
|
||||||
start = _ trigger:trigger _ guard:("[" _ guard _ "]")? _ actions:("/" _ actions )? _ {
|
start = tlabel / comment
|
||||||
return {
|
|
||||||
trigger,
|
tlabel = _ trigger:trigger _ guard:("[" _ guard _ "]")? _ actions:("/" _ actions )? _ {
|
||||||
guard: guard ? guard[2] : {kind: "literal", value: true},
|
return {
|
||||||
actions: actions ? actions[2] : [],
|
kind: "transitionLabel",
|
||||||
};
|
trigger,
|
||||||
|
guard: guard ? guard[2] : {kind: "literal", value: true},
|
||||||
|
actions: actions ? actions[2] : [],
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
trigger = afterTrigger / entryTrigger / exitTrigger / eventTrigger
|
trigger = afterTrigger / entryTrigger / exitTrigger / eventTrigger
|
||||||
|
|
@ -52,9 +55,6 @@ number = [0-9]+ {
|
||||||
return parseInt(text());
|
return parseInt(text());
|
||||||
}
|
}
|
||||||
|
|
||||||
_ "whitespace"
|
|
||||||
= [ \t\n\r]*
|
|
||||||
|
|
||||||
expr = compare
|
expr = compare
|
||||||
|
|
||||||
compare = sum:sum rest:((_ ("==" / "!=" / "<" / ">" / "<=" / ">=") _) compare)? {
|
compare = sum:sum rest:((_ ("==" / "!=" / "<" / ">" / "<=" / ">=") _) compare)? {
|
||||||
|
|
@ -114,3 +114,15 @@ boolean = ("true" / "false") {
|
||||||
raise = "^" _ event:identifier {
|
raise = "^" _ event:identifier {
|
||||||
return {kind: "raise", event};
|
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