├── .gitignore
├── LICENSE
├── README.md
├── flue.js
├── index.html
├── parser.js
├── script.js
└── style.css
/.gitignore:
--------------------------------------------------------------------------------
1 | /.vscode
2 | /debug.log
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Willem
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Flue
2 | Executable flowcharts in JavaScript using a Python-like syntax
3 |
4 | [Try it out](https://gllms.github.io/Flue)
5 |
6 | ## TODO
7 | ### Interface
8 | * [ ] Undo system
9 | * [x] Orthogonal arrows
10 | ### Parser
11 | * [ ] Augmented assignment (e.g. +=)
12 | * [ ] Lists
13 |
--------------------------------------------------------------------------------
/flue.js:
--------------------------------------------------------------------------------
1 | class Flue {
2 | constructor() {
3 | this.boxes = {};
4 | this.arrows = {};
5 |
6 | this.nextId = 0;
7 | this.nextZ = 1;
8 |
9 | this.selectedItem = undefined;
10 | this.dragElement = undefined;
11 |
12 | this.activeArrow = undefined;
13 | this.activeDot = undefined;
14 |
15 | this.dotSize = 15;
16 | this.arrowThickness = 3;
17 |
18 | this.parser = new Parser();
19 | this.running = false;
20 | this.stopped = false;
21 | this.restart = false;
22 |
23 | document.addEventListener("mousedown", (e) => {
24 | if (e.target.classList.contains("out")) {
25 | let boxId = e.target.closest(".box").getAttribute("data-boxId");
26 | let outN = [...e.target.parentNode.children].indexOf(e.target);
27 | this.addArrow(boxId, outN, e.target);
28 | this.updateArrow(this.activeArrow, this.activeDot, e.pageX, e.pageY);
29 | document.body.classList.add("arrowDragging");
30 | }
31 | else {
32 | if (this.selectedItem) this.selectedItem.classList.remove("selected");
33 | let c = e.target.closest(".box");
34 | if (c) {
35 | this.selectedItem = c;
36 | this.selectedItem.classList.add("selected");
37 |
38 | if (!e.target.matches("input")) {
39 | this.dragElement = c;
40 | this.dragElement.style.zIndex = ++this.nextZ;
41 | let r = this.dragElement.getBoundingClientRect();
42 | let id = this.dragElement.getAttribute("data-boxId");
43 | this.boxes[id].dragOffset.x = e.pageX - r.x;
44 | this.boxes[id].dragOffset.y = e.pageY - r.y;
45 | }
46 | }
47 | let a = e.target.closest(".arrow");
48 | if (a) {
49 | this.selectedItem = a;
50 | this.selectedItem.classList.add("selected");
51 | }
52 | }
53 | });
54 |
55 | document.addEventListener("mousemove", (e) => {
56 | if (this.dragElement) {
57 | e.preventDefault();
58 | let id = this.dragElement.getAttribute("data-boxId");
59 | this.boxes[id].x = e.pageX;
60 | this.boxes[id].y = e.pageY;
61 | this.updateArrows(id);
62 | }
63 | if (this.activeArrow) {
64 | if (this.activeArrow && this.arrows[this.activeArrow].el) {
65 | e.preventDefault();
66 | this.updateArrow(this.activeArrow, this.activeDot, e.pageX, e.pageY);
67 | }
68 | }
69 | });
70 |
71 | document.addEventListener("mouseup", (e) => {
72 | this.dragElement = undefined;
73 | document.body.classList.remove("arrowDragging");
74 | if (e.target.classList.contains("in") && this.activeArrow) {
75 | let boxId = parseInt(e.target.closest(".box").getAttribute("data-boxId"));
76 | this.arrows[this.activeArrow].in = boxId;
77 | this.boxes[boxId].in.push(this.activeArrow);
78 | this.updateArrow(this.activeArrow, this.activeDot, e.target);
79 | } else {
80 | if (this.activeArrow && this.arrows[this.activeArrow].el) {
81 | this.arrows[this.activeArrow].el.remove();
82 | delete this.arrows[this.activeArrow];
83 | this.boxes[this.activeDot.closest(".box").getAttribute("data-boxId")].out.pop();
84 | }
85 | }
86 | this.activeArrow = undefined;
87 | });
88 |
89 | document.addEventListener("input", (e) => {
90 | let c = e.target.closest(".box");
91 | if (c) {
92 | if (e.target.matches("input[type=text]")) {
93 | this.resizeInput(e.target);
94 | let id = e.target.closest(".box").getAttribute("data-boxId");
95 | this.updateArrows(id);
96 | this.boxes[id].value = e.target.value;
97 | }
98 | }
99 | });
100 |
101 | document.addEventListener("keydown", (e) => {
102 | if (e.key == "Delete" && !e.target.matches("input")) {
103 | if (this.selectedItem) {
104 | if (this.selectedItem.matches(".box")) this.deleteBox(this.selectedItem.getAttribute("data-boxId"));
105 | else this.deleteArrow(parseInt(this.selectedItem.getAttribute("data-arrowId")));
106 | }
107 | }
108 | });
109 |
110 | window.addEventListener("beforeunload", (e) => {
111 | if (loaded)
112 | this.save();
113 | });
114 | }
115 |
116 | addBox(type, id) {
117 | if (this.nextId < id) this.nextId = id;
118 | let box = new Box(type, this.nextId);
119 | this.boxes[this.nextId++] = box;
120 | $("#boxes").appendChild(box.el);
121 | return box;
122 | }
123 |
124 | addArrow(boxId, outN, activeDot, id) {
125 | let arrow = new Arrow();
126 | $("#arrows").appendChild(arrow.el);
127 | arrow.out = boxId;
128 | arrow.outN = outN;
129 | let d = this.nextId;
130 | if (id === undefined) {
131 | this.boxes[boxId].out.push(this.nextId);
132 | this.nextId++;
133 | }
134 | else {
135 | if (this.nextId < id)
136 | this.nextId = id;
137 | d = id;
138 | }
139 | arrow.el.setAttribute("data-arrowId", d);
140 | this.arrows[d] = arrow;
141 | this.activeArrow = d;
142 | this.activeDot = activeDot;
143 | return arrow;
144 | }
145 |
146 | updateArrow(arrow, beginPoint, endPointX, endPointY) {
147 | let arrowEnd = this.arrows[arrow].el.querySelector(".arrowEnd");
148 |
149 | let r = beginPoint.getBoundingClientRect();
150 | r.x += this.dotSize / 2;
151 | r.y += this.dotSize / 2 - this.arrowThickness / 2;
152 |
153 | let rr = endPointY == undefined ? endPointX.getBoundingClientRect() : { x: endPointX, y: endPointY };
154 | rr.x += this.dotSize / 2;
155 | rr.y += this.dotSize / 2;
156 |
157 | let dx = rr.x - r.x;
158 | let dy = rr.y - r.y;
159 |
160 | if (rr.y > r.y + 40) {
161 | if (Math.abs(dx) < 30) {
162 | this.arrows[arrow].el.querySelectorAll(".arrowPart").forEach((e) => e.style.display = "none");
163 | arrowEnd.style.transform = `rotate(${Math.atan(dy / dx) + Math.PI * (rr.x < r.x)}rad)`;
164 | let cx = r.x + dx / 2;
165 | let cy = r.y + dy / 2;
166 | let w = Math.sqrt(dx ** 2 + dy ** 2);
167 | arrowEnd.style.left = cx - w / 2 + "px";
168 | arrowEnd.style.top = cy + "px";
169 | arrowEnd.style.width = w + "px";
170 | } else {
171 | let parts = this.arrows[arrow].el.querySelectorAll(".arrowPart");
172 | parts[0].style.left = r.x + "px";
173 | parts[0].style.top = r.y + "px";
174 | let half = dy / 2;
175 | parts[0].style.height = arrowEnd.style.width = half + "px";
176 | parts[1].style.width = Math.abs(dx) + "px";
177 | parts[1].style.top = r.y + half - 3 + "px";
178 | parts[1].style.left = rr.x > r.x ? r.x + "px" : r.x + dx + "px";
179 | parts[0].style.width = parts[1].style.height = "3px";
180 |
181 | arrowEnd.style.top = r.y + half - 3 + half / 2 + "px";
182 | if (dx < 0) {
183 | arrowEnd.style.left = r.x + dx - half / 2 + "px";
184 | } else {
185 | arrowEnd.style.left = r.x + dx - half / 2 + "px";
186 | }
187 | arrowEnd.style.transform = `rotate(${Math.PI / 2}rad)`;
188 | parts[0].style.display = "block";
189 | parts[1].style.display = "block";
190 | parts[2].style.display = "none";
191 | parts[3].style.display = "none";
192 | }
193 | } else {
194 | let parts = this.arrows[arrow].el.querySelectorAll(".arrowPart");
195 | parts[0].style.left = r.x + "px";
196 | parts[0].style.top = r.y + "px";
197 | parts[0].style.height = "30px";
198 |
199 | parts[0].style.width = parts[1].style.height = parts[3].style.height = parts[2].style.width = "3px";
200 |
201 | let half = dx / 2;
202 | parts[1].style.width = Math.abs(half) + 2 + "px";
203 | parts[1].style.left = rr.x > r.x ? r.x + "px" : rr.x - half + "px";
204 | parts[1].style.top = r.y + 30 - 3 + "px";
205 |
206 | parts[3].style.width = Math.abs(half) + 1 + "px";
207 | parts[3].style.left = rr.x > r.x ? rr.x - half + "px" : rr.x - 2 + "px";
208 | parts[3].style.top = rr.y - 30 + "px";
209 |
210 | parts[2].style.top = rr.y - 30 + "px";
211 | // parts[2].style.left = rr.x > r.x ? r.x + half + "px" : rr.x - half + "px"
212 | parts[2].style.left = r.x + half - 1 + "px";
213 | parts[2].style.height = -dy + 60 + "px";
214 |
215 | arrowEnd.style.width = 30 - 3 + "px";
216 | arrowEnd.style.top = rr.y - 30 + 15 - 1.5 + "px";
217 | arrowEnd.style.left = rr.x > r.x ? rr.x - 15 + 1.5 + "px" : rr.x - 15 + 1.5 + "px";
218 | arrowEnd.style.transform = `rotate(${Math.PI / 2}rad)`;
219 |
220 | parts[0].style.display = "block";
221 | parts[1].style.display = "block";
222 | parts[2].style.display = "block";
223 | parts[3].style.display = "block";
224 | }
225 | }
226 |
227 | updateArrows(boxId) {
228 | let b = this.boxes[boxId];
229 | for (let i of [...new Set(b.in.concat(b.out))]) {
230 | this.updateArrow(i, this.boxes[this.arrows[i].out].el.querySelectorAll(".out")[this.arrows[i].outN], this.boxes[this.arrows[i].in].el.querySelector(".in"));
231 | }
232 | }
233 |
234 | resizeInput(e) {
235 | e.style.width = e.value.length + .2 + "ch";
236 | }
237 |
238 | deleteBox(boxId) {
239 | let b = this.boxes[boxId];
240 | for (let i of [...new Set(b.in.concat(b.out))]) {
241 | this.deleteArrow(i);
242 | }
243 | b.el.remove();
244 | delete this.boxes[boxId];
245 | this.selectedItem = undefined;
246 | }
247 |
248 | deleteArrow(arrowId) {
249 | let a = this.arrows[arrowId];
250 | let out = this.boxes[a.out].out;
251 | out.splice(out.indexOf(arrowId), 1);
252 | let inn = this.boxes[a.in].in;
253 | inn.splice(inn.indexOf(arrowId), 1);
254 | a.el.remove();
255 | delete this.arrows[arrowId];
256 | }
257 |
258 | updateScope() {
259 | $("#scope").innerHTML = "";
260 | for (const e in this.parser.scope) {
261 | let val = this.parser.scope[e];
262 | if (typeof val == "string") val = "\"" + val + "\"";
263 | $("#scope").innerHTML += `
${e} | ${val} |
`;
264 | }
265 | }
266 |
267 | run() {
268 | if (this.running) {
269 | this.stopped = this.restart = true;
270 | return;
271 | }
272 | this.running = true;
273 | this.parser.scope = Object.assign({}, this.parser.builtins);
274 | $("#scope").innerHTML = "";
275 | $("#console").innerHTML = "";
276 | let list = [];
277 | list = list.concat(this.runBox(0));
278 | (function () {
279 | function runNext() {
280 | if (list.length > 0 && !this.stopped) {
281 | list = list.concat(this.runBox(list[0]));
282 | list.shift();
283 | setTimeout(() => runNext.call(this), 0);
284 | } else {
285 | this.stopped = this.running = false;
286 | if (this.restart) {
287 | this.restart = false;
288 | this.run();
289 | }
290 | }
291 | }
292 | runNext.call(this);
293 | }).call(this);
294 | }
295 |
296 | runBox(boxId) {
297 | let b = this.boxes[boxId];
298 | let as;
299 | switch (b.type) {
300 | case "start":
301 | as = b.out;
302 | break;
303 | case "end":
304 | as = [];
305 | break;
306 | case "statement":
307 | this.parser.run(this.parser.parse(this.parser.tokenize(b.value)));
308 | this.updateScope();
309 | as = b.out;
310 | break;
311 | case "conditional":
312 | as = b.out.filter((e) => this.arrows[e].outN == this.parser.run(this.parser.parse(this.parser.tokenize(b.value))) ? 0 : 1);
313 | break;
314 | case "input":
315 | b.value.split(",").forEach((e) => {
316 | e = e.trim();
317 | this.parser.scope[e] = prompt(e + ":");
318 | });
319 | this.updateScope();
320 | as = b.out;
321 | break;
322 | case "output":
323 | $("#console").innerHTML += this.parser.run(this.parser.parse(this.parser.tokenize(b.el.querySelector("input").value))) + "
";
324 | $("#console").scrollTo(0, $("#console").scrollHeight);
325 | as = b.out;
326 | break;
327 | default:
328 | alert("ERROR");
329 | as = b.out ? b.out : [];
330 | }
331 |
332 | let bs = [];
333 | as.forEach(function (e) {
334 | bs.push(this.arrows[e].in);
335 | }, this);
336 | return bs;
337 | }
338 |
339 | stop() {
340 | if (this.running)
341 | this.stopped = true;
342 | }
343 |
344 | save() {
345 | localStorage.setItem("flue-save", JSON.stringify({ boxes: this.boxes, arrows: this.arrows }));
346 | }
347 | }
348 |
349 | class Box {
350 | constructor(type = "statement", id = -1) {
351 | this.type = type;
352 | this.id = id;
353 | this.el = document.createElement("div");
354 | this.el.classList.add("box", type);
355 | this.el.setAttribute("data-boxId", this.id);
356 | this.dragOffset = { x: 0, y: 0 };
357 | this.value = "";
358 |
359 | this._x = window.innerWidth / 2 / 4;
360 | this._y = 0;
361 | switch (type) {
362 | case "start":
363 | this.el.innerHTML = `START`;
364 | break;
365 | case "end":
366 | this.el.innerHTML = `END`;
367 | break;
368 | case "statement":
369 | this.el.innerHTML = ``;
370 | this.value = "x = 1";
371 | break;
372 | case "conditional":
373 | this.el.innerHTML = `YesNo
`;
374 | this.value = "x < 5";
375 | break;
376 | case "input":
377 | this.el.innerHTML = ``;
378 | this.value = "p, q";
379 | break;
380 | case "output":
381 | this.el.innerHTML = ``;
382 | this.value = "x";
383 | }
384 | this.out = [];
385 | this.in = [];
386 | }
387 |
388 | set x(val) {
389 | this._x = val - this.dragOffset.x;
390 | this.el.style.left = this._x + "px";
391 | }
392 |
393 | set y(val) {
394 | this._y = val - this.dragOffset.y;
395 | this.el.style.top = this._y + "px";
396 | }
397 | }
398 |
399 | class Arrow {
400 | constructor() {
401 | this.el = document.createElement("div");
402 | this.el.classList.add("arrow");
403 | this.el.innerHTML = ``;
404 | }
405 | }
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Flue
8 |
9 |
10 |
11 |
12 |
26 |
30 |
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/parser.js:
--------------------------------------------------------------------------------
1 | class Parser {
2 | constructor() {
3 | this.builtins = {
4 | "int": (e) => parseInt(e),
5 | "float": (e) => parseFloat(e),
6 | "str": (e) => String(e),
7 | "add": (a, b) => a + b,
8 | "sub": (a, b) => a - b
9 | };
10 |
11 | this.scope = Object.assign({}, this.builtins);
12 |
13 | this.operators = {
14 | binary: {
15 | "=": {
16 | symbol: "=",
17 | precedence: 0,
18 | associativity: "rtl",
19 | run: (a, b) => this.scope[a] = b
20 | },
21 | ":=": {
22 | symbol: ":=",
23 | precedence: 0,
24 | associativity: "rtl",
25 | run: (a, b) => this.scope[a] = b
26 | },
27 | "or": {
28 | symbol: "or",
29 | precedence: 3,
30 | associativity: "ltr",
31 | run: (a, b) => a || b,
32 | },
33 | "and": {
34 | symbol: "and",
35 | precedence: 4,
36 | associativity: "ltr",
37 | run: (a, b) => a && b
38 | },
39 | "<": {
40 | symbol: "<",
41 | precedence: 6,
42 | associativity: "ltr",
43 | run: (a, b) => a < b
44 | },
45 | "<=": {
46 | symbol: "<=",
47 | precedence: 6,
48 | associativity: "ltr",
49 | run: (a, b) => a <= b
50 | },
51 | ">": {
52 | symbol: ">",
53 | precedence: 6,
54 | associativity: "ltr",
55 | run: (a, b) => a > b
56 | },
57 | ">=": {
58 | symbol: ">=",
59 | precedence: 6,
60 | associativity: "ltr",
61 | run: (a, b) => a >= b
62 | },
63 | "!=": {
64 | symbol: "!=",
65 | precedence: 6,
66 | associativity: "ltr",
67 | run: (a, b) => a != b
68 | },
69 | "==": {
70 | symbol: "==",
71 | precedence: 6,
72 | associativity: "ltr",
73 | run: (a, b) => a == b
74 | },
75 | "+": {
76 | symbol: "+",
77 | precedence: 11,
78 | associativity: "ltr",
79 | run: (a, b) => a + b
80 | },
81 | "-": {
82 | symbol: "-",
83 | precedence: 11,
84 | associativity: "ltr",
85 | run: (a, b) => a - b
86 | },
87 | "*": {
88 | symbol: "*",
89 | precedence: 12,
90 | associativity: "ltr",
91 | run: (a, b) => a * b
92 | },
93 | "/": {
94 | symbol: "/",
95 | precedence: 12,
96 | associativity: "ltr",
97 | run: (a, b) => a / b
98 | },
99 | "//": {
100 | symbol: "//",
101 | precedence: 12,
102 | associativity: "ltr",
103 | run: (a, b) => Math.floor(a / b)
104 | },
105 | "%": {
106 | symbol: "%",
107 | precedence: 12,
108 | associativity: "ltr",
109 | run: (a, b) => a % b
110 | },
111 | "**": {
112 | symbol: "**",
113 | precedence: 14,
114 | associativity: "rtl",
115 | run: (a, b) => a ** b
116 | }
117 | },
118 | unary: {
119 | "not": {
120 | symbol: "not",
121 | precedence: 5,
122 | associativity: "ltr",
123 | run: (a) => !a,
124 | unary: true
125 | },
126 | "+": {
127 | symbol: "+",
128 | precedence: 13,
129 | associativity: "ltr",
130 | run: (a) => +a,
131 | unary: true
132 | },
133 | "-": {
134 | symbol: "-",
135 | precedence: 13,
136 | associativity: "ltr",
137 | run: (a) => -a,
138 | unary: true
139 | }
140 | }
141 | };
142 |
143 | this.atoms = [
144 | {
145 | name: "number",
146 | match: /^\d+(\.\d+)?/
147 | }, {
148 | name: "string",
149 | match: /^("[^"]*")|('[^']*')/
150 | }, {
151 | name: "boolean",
152 | match: ["True", "False"]
153 | }, {
154 | name: "operator",
155 | match: Object.keys(this.operators.binary).concat(Object.keys(this.operators.unary)).sort((a, b) => b.length - a.length)
156 | }, {
157 | name: "variable",
158 | match: /^[\w_]+/
159 | }, {
160 | name: "leftBracket",
161 | match: ["("]
162 | }, {
163 | name: "rightBracket",
164 | match: [")"]
165 | }, {
166 | name: "comma",
167 | match: [","]
168 | }
169 | ];
170 | }
171 |
172 | tokenize(input) {
173 | let tokens = [];
174 | let cursorPos = 0;
175 | let slice = input.trim();
176 | while (slice.length > 0) {
177 | let found;
178 | for (let a of this.atoms) {
179 | if (Array.isArray(a.match)) {
180 | for (let m of a.match) {
181 | if (slice.startsWith(m)) {
182 | found = { "name": a.name, "value": m };
183 | break;
184 | }
185 | }
186 | } else {
187 | let match = slice.match(a.match);
188 | if (match !== null) {
189 | found = { "name": a.name, "value": match[0] };
190 | break;
191 | }
192 | }
193 | if (found) break;
194 | }
195 | if (found) {
196 | tokens.push(found);
197 | cursorPos += found.value.length;
198 | while (input[cursorPos] == " ") cursorPos++;
199 | slice = input.slice(cursorPos);
200 | } else {
201 | throw new SyntaxError();
202 | }
203 | }
204 | return tokens;
205 | }
206 |
207 | parse(tokens) {
208 | let tree = {
209 | args: []
210 | };
211 | let track = [tree];
212 | let currentTree = tree;
213 | for (let token of tokens) {
214 | switch (token.name) {
215 | case "operator":
216 | if (currentTree.args.length > 0) {
217 | if (currentTree.operator === undefined) {
218 | currentTree.operator = this.operators.binary[token.value];
219 | } else {
220 | let operator = this.operators.binary[token.value];
221 | if (operator.precedence > currentTree.operator.precedence || operator.precedence == currentTree.operator.precedence && operator.associativity == "rtl") {
222 | let lastValue = currentTree.args.pop();
223 | let newTree = {
224 | operator: operator,
225 | args: [lastValue]
226 | };
227 | currentTree.args.push(newTree);
228 | track.push(newTree);
229 | currentTree = newTree;
230 | } else {
231 | let newTree = Object.assign({}, currentTree);
232 | currentTree.operator = operator;
233 | currentTree.args = [newTree];
234 | }
235 | }
236 | } else {
237 | currentTree.operator = this.operators.unary[token.value];
238 | }
239 |
240 | if (currentTree.operator && currentTree.operator.unary) {
241 | track.pop();
242 | currentTree = track[track.length - 1];
243 | }
244 | break;
245 | case "leftBracket":
246 | let newTree = {
247 | args: []
248 | };
249 | let lastArg = currentTree.args[currentTree.args.length - 1];
250 | if (lastArg && typeof lastArg == "object" && lastArg.name != "operator") {
251 | let caller = currentTree.args.pop();
252 | let newTree = {
253 | operator: {
254 | symbol: "call",
255 | precedence: .6,
256 | associativity: "ltr"
257 | },
258 | args: [caller]
259 | };
260 | currentTree.args.push(newTree);
261 | track.push(newTree);
262 | currentTree = newTree;
263 | }
264 | if (currentTree.args.length > 0) {
265 | currentTree.args.push(newTree);
266 | track.push(newTree);
267 | currentTree = newTree;
268 | } else {
269 | currentTree.operator = newTree.operator;
270 | }
271 | break;
272 | case "rightBracket":
273 | track.pop();
274 | currentTree = track[track.length - 1];
275 | break;
276 | case "comma":
277 | track.pop();
278 | currentTree = track[track.length - 1];
279 | if (currentTree.operator != "tuple") {
280 | let firstElement = currentTree.args.pop();
281 | let newTree = {
282 | operator: {
283 | symbol: "tuple",
284 | precedence: .5
285 | },
286 | args: [firstElement]
287 | };
288 | currentTree.args.push(newTree);
289 | track.push(newTree);
290 | currentTree = newTree;
291 | }
292 | break;
293 | default:
294 | currentTree.args.push({ name: token.name, value: token.value });
295 | break;
296 | }
297 | }
298 | if (tree.operator === undefined) return tree.args[0];
299 | return tree;
300 | }
301 |
302 | run(ast) {
303 | if (!ast.operator && ast.args) return this.run(ast.args[0]);
304 | let args = [];
305 | if ("value" in ast) {
306 | switch (ast.name) {
307 | case "number":
308 | return parseFloat(ast.value);
309 | case "string":
310 | return ast.value.slice(1, ast.value.length - 1); // remove quotation marks
311 | case "boolean":
312 | return ast.value == "True";
313 | case "variable":
314 | return this.scope[ast.value];
315 | }
316 | }
317 | if (ast.operator.symbol == "call") {
318 | let caller = ast.args[0];
319 | let finalArgs = [];
320 | if (ast.args[1]) {
321 | ast.args[1].args.forEach((e) => {
322 | if (Array.isArray(e)) finalArgs.push(e);
323 | else finalArgs.push(this.run(e));
324 | });
325 | }
326 | return this.scope[caller.value](...finalArgs);
327 | }
328 | ast.args.forEach((e) => {
329 | if (Array.isArray(e)) args.push(e);
330 | else args.push(this.run(e));
331 | });
332 | if (ast.operator.symbol == "=") {
333 | return ast.operator.run(ast.args[0].value, args[1]);
334 | }
335 | return ast.operator.run(...args);
336 | }
337 | }
338 |
339 | let parser = new Parser();
340 | parser.run(parser.parse(parser.tokenize("sub(4*5, 2*3)")));
--------------------------------------------------------------------------------
/script.js:
--------------------------------------------------------------------------------
1 | const $ = (e) => document.querySelector(e);
2 |
3 | const flue = new Flue();
4 |
5 | let loaded = true;
6 | let save = localStorage.getItem("flue-save");
7 | if (save != null) {
8 | loaded = false;
9 | save = JSON.parse(save);
10 | for (let b in save.boxes) {
11 | let box = flue.addBox(save.boxes[b].type, b);
12 | box.out = save.boxes[b].out;
13 | box.in = save.boxes[b].in;
14 | box.x = save.boxes[b]._x;
15 | box.y = save.boxes[b]._y;
16 | box.value = save.boxes[b].value;
17 | let input = box.el.querySelector("input");
18 | if (input) {
19 | input.value = save.boxes[b].value;
20 | flue.resizeInput(input);
21 | }
22 | }
23 | for (let b in flue.boxes) {
24 | flue.boxes[b].out.forEach((id) => {
25 | let arrow = flue.addArrow(flue.boxes[b].id, save.arrows[id].outN, flue.boxes[b].el.querySelector(".dotrow.bottom").children[save.arrows[id].outN], id);
26 | arrow.in = save.arrows[id].in;
27 | });
28 | }
29 | flue.activeArrow = undefined;
30 | for (let b in flue.boxes) {
31 | flue.updateArrows(flue.boxes[b].id);
32 | }
33 | } else {
34 | let center = window.innerWidth / 2;
35 |
36 | let start = flue.addBox("start");
37 | let end = flue.addBox("end");
38 |
39 | start.x = end.x = center - 100;
40 | start.y = 90;
41 | end.y = 400;
42 | }
43 |
44 |
--------------------------------------------------------------------------------
/style.css:
--------------------------------------------------------------------------------
1 | body,
2 | input,
3 | button,
4 | #console {
5 | font-family: "Lucida Console", "Consolas", monospace;
6 | }
7 |
8 | .box input,
9 | .box span {
10 | background: none;
11 | border: none;
12 | font-size: 24px;
13 | min-width: 1ch;
14 | text-align: center;
15 | }
16 |
17 | button {
18 | width: 50px;
19 | height: 30px;
20 | font-size: 18px;
21 | background: #ffa726;
22 | border: none;
23 | }
24 |
25 | button:focus {
26 | outline: none;
27 | filter: brightness(1.2);
28 | }
29 |
30 | button:active {
31 | filter: brightness(.8);
32 | }
33 |
34 | #statementButton {
35 | background: #ba68c8;
36 | }
37 |
38 | #conditionalButton {
39 | background: #64b5f6;
40 | clip-path: polygon(10% 0, 90% 0, 100% 50%, 90% 100%, 10% 100%, 0 50%);
41 | }
42 |
43 | #inputButton {
44 | background: #81c784;
45 | clip-path: polygon(0 0, 100% 0, 80% 100%, 20% 100%);
46 | }
47 |
48 | #outputButton {
49 | background: #81c784;
50 | clip-path: polygon(20% 0, 80% 0, 100% 100%, 0 100%);
51 | }
52 |
53 | #stopButton {
54 | width: unset;
55 | }
56 |
57 | .box {
58 | position: absolute;
59 | padding: 1em;
60 | filter: drop-shadow(2px 2px 5px rgba(0, 0, 0, 0.2));
61 | background: #ba68c8;
62 | height: 25px;
63 | cursor: pointer;
64 | user-select: none;
65 | z-index: 1;
66 | }
67 |
68 | .arrowDragging .box {
69 | cursor: pointer;
70 | }
71 |
72 | .box.selected {
73 | filter: brightness(1.2);
74 | }
75 |
76 | .box.conditional {
77 | padding-bottom: 30px;
78 | padding-left: 2em;
79 | padding-right: 2em;
80 | background: #64b5f6;
81 | }
82 |
83 | .box.conditional:before,
84 | .box.conditional:after,
85 | .box.input:before,
86 | .box.input:after,
87 | .box.output:before,
88 | .box.output:after {
89 | width: 0;
90 | height: 0;
91 | top: 0;
92 | content: "";
93 | border-style: solid;
94 | position: absolute;
95 | }
96 |
97 | .box.conditional:before {
98 | left: -30px;
99 | border-color: transparent #64b5f6 transparent transparent;
100 | border-width: 35.5px 30px 35.5px 0;
101 | }
102 |
103 | .box.conditional:after {
104 | right: -30px;
105 | border-color: transparent transparent transparent #64b5f6;
106 | border-width: 35.5px 0 35.5px 30px;
107 | }
108 |
109 | .box.input, .box.output {
110 | background: #81c784;
111 | }
112 |
113 | .box.input:before {
114 | left: -30px;
115 | border-color: transparent #81c784 transparent transparent;
116 | border-width: 0 30px 57px 0;
117 | }
118 |
119 | .box.input:after {
120 | right: -30px;
121 | border-color: #81c784 transparent transparent transparent;
122 | border-width: 57px 30px 0 0;
123 | }
124 |
125 | .box.output:before {
126 | left: -30px;
127 | border-color: transparent transparent #81c784 transparent;
128 | border-width: 0 0 57px 30px;
129 | }
130 |
131 | .box.output:after {
132 | right: -30px;
133 | border-color: transparent transparent transparent #81c784;
134 | border-width: 57px 0 0 30px;
135 | }
136 |
137 | .box.conditional .yesno {
138 | display: flex;
139 | justify-content: space-around;
140 | position: absolute;
141 | width: 100%;
142 | bottom: 0.5em;
143 | left: 0;
144 | }
145 |
146 | .box span {
147 | display: block;
148 | width: 100%;
149 | text-align: center;
150 | }
151 |
152 | .box.start,
153 | .box.end {
154 | background: #ffa726;
155 | width: 100px;
156 | border-radius: 25%/50%;
157 | }
158 |
159 | .dotrow {
160 | position: absolute;
161 | bottom: -7.5px;
162 | display: inline-flex;
163 | width: 100%;
164 | height: 15px;
165 | left: 0;
166 | justify-content: space-around;
167 | pointer-events: none;
168 | }
169 |
170 | .dotrow.top {
171 | top: -7.5px;
172 | }
173 |
174 | .dotrow.bottom {
175 | bottom: -7.5px;
176 | }
177 |
178 | .dot {
179 | display: inline-block;
180 | margin-left: auto;
181 | margin-right: auto;
182 | left: 0;
183 | right: 0;
184 | width: 15px;
185 | height: 15px;
186 | background: #3d5afe;
187 | border-radius: 50%;
188 | opacity: 0;
189 | cursor: pointer;
190 | pointer-events: auto;
191 | }
192 |
193 | .box:hover .dot, .dot:hover {
194 | opacity: 1;
195 | }
196 |
197 | .dot.in {
198 | top: -7.5px;
199 | }
200 |
201 | .dot.out {
202 | bottom: -7.5px;
203 | }
204 |
205 | .arrow .arrowPart, .arrow .arrowEnd {
206 | position: absolute;
207 | background: black;
208 | pointer-events: auto;
209 | cursor: pointer;
210 | }
211 |
212 | .arrow .arrowPart:nth-child(2), .arrow .arrowPart:nth-child(3), .arrow .arrowPart:nth-child(4) {
213 | z-index: auto;
214 | }
215 |
216 | .arrow .arrowEnd {
217 | height: 3px;
218 | }
219 |
220 | .arrow.selected * {
221 | background: #3d5afe;
222 | }
223 |
224 | .arrowDragging .arrow * {
225 | pointer-events: none;
226 | }
227 |
228 | .arrow .arrowEnd:before, .arrow .arrowPart:before {
229 | content: "";
230 | position: absolute;
231 | border-radius: 9px;
232 | z-index: -1;
233 | }
234 |
235 | .arrow .arrowEnd:before, .arrow .arrowPart:nth-child(2):before, .arrow .arrowPart:nth-child(4):before {
236 | width: 100%;
237 | height: 20px;
238 | top: -9px;
239 | }
240 |
241 | .arrow .arrowPart:nth-child(1):before, .arrow .arrowPart:nth-child(3):before {
242 | height: calc(100% + 8px);
243 | width: 20px;
244 | left: -9px;
245 | }
246 |
247 | .arrow .arrowPart:nth-child(4):before {
248 | width: calc(18px + 100%);
249 | left: -9px;
250 | }
251 |
252 | .arrow .arrowEnd:before {
253 | width: calc(9px + 100%);
254 | left: -9px;
255 | }
256 |
257 | .arrow .arrowEnd:after {
258 | border: 7.5px solid transparent;
259 | border-left: 15px solid black;
260 | content: "";
261 | position: absolute;
262 | top: -6px;
263 | right: -9px;
264 | }
265 |
266 | .arrow.selected .arrowEnd:after {
267 | border-left-color: #3d5afe;
268 | }
269 |
270 | #console {
271 | background: black;
272 | color: white;
273 | height: 10em;
274 | width: 15em;
275 | overflow-y: auto;
276 | font-size: 18px;
277 | padding: 1em;
278 | }
279 |
280 | table {
281 | border-collapse: collapse;
282 | }
283 | td {
284 | border: 2px solid #ccc;
285 | padding: .5em 1em;
286 | min-width: 3em;
287 | text-align: right;
288 | }
--------------------------------------------------------------------------------