├── .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 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |

Scope

20 | 21 | 22 |
23 |

Console

24 |
25 |
26 |
27 |
28 |
29 |
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 | } --------------------------------------------------------------------------------