├── .github └── workflows │ └── bean-tests.yml ├── .gitignore ├── LICENSE ├── README.md ├── doc └── Trellis.md ├── package-lock.json ├── package.json ├── public ├── _redirects ├── css │ └── style.css ├── demos.edn ├── fonts │ ├── SpaceGrotesk-Bold.fnt │ ├── SpaceGrotesk-Bold.png │ ├── SpaceGrotesk.fnt │ ├── SpaceGrotesk.png │ └── SpaceGrotesk.ttf ├── help-dark.png ├── help.png ├── img │ ├── code-icon.png │ ├── eraser.png │ ├── frame-icon.png │ ├── left-label.png │ ├── logo.png │ ├── logo.svg │ ├── made-frame-icon.png │ ├── skip-label.png │ ├── stripes.jpg │ ├── top-label.png │ ├── top-left-label.png │ └── trash-icon.png └── index.html ├── shadow-cljs.edn ├── src └── bean │ ├── area.cljs │ ├── code.cljs │ ├── code_errors.cljs │ ├── deps.cljs │ ├── errors.cljs │ ├── frames.cljs │ ├── functions.cljs │ ├── grid.cljs │ ├── interpreter.cljs │ ├── log.cljs │ ├── operators.cljs │ ├── parser │ ├── parser.cljs │ └── trellis_parser.cljs │ ├── provenance.cljs │ ├── trellis.cljs │ ├── ui │ ├── db.cljs │ ├── demos.cljs │ ├── events.cljs │ ├── features.cljs │ ├── interceptors.cljs │ ├── llm.cljs │ ├── main.cljs │ ├── paste.cljs │ ├── provenance.cljs │ ├── routes.cljs │ ├── save.cljs │ ├── styles.cljs │ ├── subs.cljs │ ├── util.cljs │ └── views │ │ ├── code.cljs │ │ ├── help.cljs │ │ ├── popups.cljs │ │ ├── root.cljs │ │ ├── sheet.cljs │ │ └── sidebar.cljs │ ├── util.cljs │ └── value.cljs └── test ├── bean ├── code_test.cljs ├── core_test.cljs ├── deps_test.cljs ├── frames_test.cljs ├── grid_test.cljs ├── interpreter_test.cljs ├── operators_test.cljs ├── parser │ └── trellis_parser_test.cljs ├── parser_test.cljs ├── provenance_test.cljs ├── trellis_test.cljs └── value_test.cljs └── trellis ├── addition_test.leaf ├── basic_evaluation.leaf ├── function_invocation.leaf ├── inlined_function_invocation.leaf ├── matrix_evaluation.leaf ├── matrix_spill_conflict_error.leaf ├── matrix_spill_error.leaf ├── returns_errors.leaf └── spillage_dependency.leaf /.github/workflows/bean-tests.yml: -------------------------------------------------------------------------------- 1 | name: Bean tests 2 | on: 3 | push: 4 | branches: ["main"] 5 | pull_request: 6 | branches: ["main"] 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v3 13 | - name: Get Node.js 14 | uses: actions/setup-node@v3 15 | with: 16 | node-version: 18.x 17 | cache: 'npm' 18 | - run: npm install 19 | - run: npx shadow-cljs compile test && node tests.js 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | public/js 3 | 4 | /target 5 | /checkouts 6 | /src/gen 7 | 8 | pom.xml 9 | pom.xml.asc 10 | *.iml 11 | *.jar 12 | *.log 13 | .shadow-cljs 14 | .idea 15 | .lein-* 16 | .nrepl-* 17 | .DS_Store 18 | 19 | .hgignore 20 | .hg/ 21 | 22 | .clj-kondo/ 23 | .lsp/ 24 | output.js 25 | tests.js 26 | .calva/ 27 | out/ 28 | .vscode/ 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bean 2 | 3 | Spreadsheets are an outstanding tool for building small software. However, mainstream spreadsheet software lacks the tools to create spreadsheets that are maintainable, error-free and easy to work with – spreadsheets that are "production-ready". 4 | 5 | Bean is an exploration into new/overlooked ideas in the spreadsheet paradigm to solve this problem. It's intended to be a playground for trying out features (and _maybe_ eventually become a full-fledged spreadsheet software). 6 | 7 | This involves looking at spreadsheets as a programming environment from the ground up. Read [this blogpost](https://bean.nilenso.com/blog/posts/spreadsheets-and-small-software/) for the full background. [This talk](https://www.youtube.com/watch?v=0yKf8TrLUOw) is a great introduction to the problem. 8 | 9 | [Play around with it](https://bean.nilenso.com) (it's super early). 10 | 11 | ## State of affairs 12 | 13 | **Updated on:** 31-Oct-2023 14 | 15 | Bean has these basics in place: a grid, a parser, an interpreter for a small formula language and reactive recalculation (with dynamic arrays). 16 | 17 | Please shoot us an email at bean @ nilenso dot com and we'd be happy to walk you through. 18 | 19 | We are currently trying out the following features 20 | - A "secondary space" for linear thinking 21 | - Generating provenance for all calculations for auditability 22 | - Flexible tables as a primary data structure 23 | 24 | A repository with the accompanying research work will be put up soon. 25 | 26 | ## Setting up 27 | 28 | Bean is written in [ClojureScript](https://clojurescript.org/). You'll need [java](https://www.java.com/en/download/), [npm and nodejs](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) installed. Then run 29 | 30 | ``` 31 | npm install 32 | npx shadow-cljs watch ui 33 | ``` 34 | 35 | You can access Bean running locally at http://localhost:8090. 36 | 37 | Installation instructions for Java can be found [here](https://github.com/supertokens/supertokens-core/wiki/Installing-OpenJDK-for-Mac-and-Linux). 38 | 39 | #### Tests 40 | ``` 41 | npx shadow-cljs compile test && node tests.js 42 | ``` 43 | 44 | ## Authors 45 | - [Prabhanshu Gupta](https://github.com/prabhanshuguptagit) 46 | - [Ravi Chandra Padmala](https://github.com/neenaoffline) 47 | -------------------------------------------------------------------------------- /doc/Trellis.md: -------------------------------------------------------------------------------- 1 | # Trellis 2 | 3 | ## Leaf file format 4 | 5 | Trellis runs .leaf files. This format is going to change considerably over the 6 | next couple of months. The current usecase it solves is representing a sheet with 7 | its associated tests. 8 | 9 | The file consists of three sections separated by `\n\n%\n\n`: 10 | 11 | 1. Code section 12 | 2. Grid section 13 | 3. Tests section 14 | 15 | ### Code section 16 | 17 | This section has the code as text. 18 | 19 | eg. 20 | 21 | ``` 22 | foo:10 23 | add:{x+y} 24 | ``` 25 | 26 | ### Grid section 27 | 28 | This section has the sheet's grid contents in CSV form. 29 | 30 | eg. 31 | 32 | ``` 33 | 1,,3,4 34 | 5,,7,8 35 | =A1:A3,,, 36 | ``` 37 | 38 | Due to quirks with the current parser implementation, each row must have atleast 39 | one `,`. 40 | 41 | ### Tests section 42 | 43 | This section defines a new language for the test definitions. Each line 44 | represents a test statement of the form `expression = expression`. eg. 45 | 46 | ``` 47 | A1 = 1 48 | B1 = "" 49 | C1=3 50 | D1=4 51 | A3=1 52 | A1+A2=6 53 | ``` 54 | 55 | ## Example 56 | 57 | All together, a .leaf looks like... 58 | 59 | ``` 60 | namedten: 10 61 | namedfifty: 50 62 | 63 | % 64 | 65 | 0,1,=A1+B1,=A1+B1+C1 66 | 4,5,6,7 67 | 68 | % 69 | 70 | A1 = 0 71 | B1 = C1 72 | C1 = 1 73 | D1 = 2 74 | ``` 75 | 76 | Note that the parser is currently quite unforgiving and you'll need to be 77 | specific about not leaving stray newlines or spaces around. 78 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bean", 3 | "version": "0.0.1", 4 | "private": true, 5 | "prepare": "husky install", 6 | "devDependencies": { 7 | "shadow-cljs": "2.25.2" 8 | }, 9 | "dependencies": { 10 | "pixi-viewport": "^5.0.2", 11 | "pixi.js": "^7.3.3", 12 | "react": "^18.2.0", 13 | "react-dom": "^18.2.0" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /public/_redirects: -------------------------------------------------------------------------------- 1 | /blog/* https://bean-blog.netlify.app/blog/:splat 200! 2 | 3 | -------------------------------------------------------------------------------- /public/css/style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --sheet-background: #fffefc; 3 | --sheet-error: #b93333; 4 | --headings-background: #f2f2f1; 5 | --cell-background: white; 6 | --corner-background: #ddd; 7 | --code-background: #fff; 8 | --code-foreground: #111; 9 | --code-margin: #dddddd; 10 | --code-pending: #f0c2b0; 11 | --sheet-border: #dedbd7; 12 | --headings-border: #8a868440; 13 | --code-lines: #ccc; 14 | --btn-foreground: #666; 15 | --btn-background: #e9e9ed; 16 | --cell-selector: #777; 17 | --link-color: rgb(63, 129, 215); 18 | --cell-height: 30px; 19 | --cell-width: 110px; 20 | --cell-padding: 5px; 21 | --cell-border: 1px; 22 | --code-width: 300px; 23 | --code-thick-line-gap: 4; 24 | --headings-left-width: 40px; 25 | --controls-height: 60px; 26 | --sidebar-left-margin: 10px; 27 | } 28 | 29 | [data-theme='dark'] { 30 | --sheet-background: #073642; 31 | --corner-background: var(--headings-background); 32 | --sheet-error: #dc322f; 33 | --headings-background: #002b36; 34 | --cell-background: #073642; 35 | --code-background: #073642; 36 | --code-foreground: #93a1a1; 37 | --code-margin: #cb4b16; 38 | --code-pending: #73402c; 39 | --sheet-border: #002b36; 40 | --sheet-header-border: #002b36; 41 | --headings-border: #073642; 42 | --code-lines: #93a1a1; 43 | --btn-foreground: white; 44 | --btn-background: var(--sheet-background); 45 | --cell-selector: #666; 46 | --link-color: rgb(164, 183, 146); 47 | } 48 | 49 | @font-face { 50 | font-family: SpaceGrotesk; 51 | src: url(/fonts/SpaceGrotesk.ttf); 52 | } 53 | 54 | body { 55 | margin: 0; 56 | background: var(--sheet-background); 57 | overflow: hidden; 58 | font-family: system-ui, -apple-system, BlinkMacSystemFont, sans-serif; 59 | font-size: 0.8rem; 60 | } 61 | 62 | a { 63 | color: var(--link-color) 64 | } 65 | 66 | p { 67 | font-family: system-ui, -apple-system, BlinkMacSystemFont, sans-serif; 68 | color: var(--btn-foreground); 69 | font-size: 0.8rem; 70 | } 71 | 72 | /* TODO: Fix code margin z index */ 73 | .container { 74 | position: relative; 75 | display: grid; 76 | grid-template-columns: var(--code-width) auto; 77 | height: 100vh; 78 | width: 100vw; 79 | } 80 | 81 | .sheet-container { 82 | position: relative; 83 | } 84 | 85 | #grid-container { 86 | position: relative; 87 | height: 100vh; 88 | width: 100%; 89 | } 90 | 91 | #cell-input { 92 | z-index: 0; 93 | background: var(--cell-background); 94 | outline: 0.5px solid var(--sheet-border); 95 | padding: var(--cell-padding); 96 | padding-top: 6px; 97 | font-family: "SpaceGrotesk"; 98 | overflow: hidden; 99 | cursor: cell; 100 | box-sizing: border-box; 101 | outline: 2px solid var(--cell-selector); 102 | position: absolute; 103 | font-size: 14px; 104 | top: 0; 105 | left: 0; 106 | transform-origin: top left; 107 | } 108 | 109 | .sidebar::before { 110 | z-index: 4; 111 | position: absolute; 112 | content: ''; 113 | left: var(--sidebar-left-margin); 114 | height: 100%; 115 | width: 1px; 116 | background: var(--code-margin); 117 | } 118 | 119 | .logo-container { 120 | height: var(--controls-height); 121 | display: flex; 122 | } 123 | 124 | .bean-logo { 125 | height: 30px; 126 | margin: auto 0 auto calc(var(--sidebar-left-margin) + 6px); 127 | } 128 | 129 | .frame-icon { 130 | height: 0.8rem; 131 | margin: auto 5px auto 0; 132 | } 133 | 134 | .code-icon { 135 | height: 0.8rem; 136 | margin: auto 7px auto 2px; 137 | } 138 | 139 | .frames-header { 140 | top: 0; 141 | height: var(--cell-height); 142 | width: 100%; 143 | padding: 0px; 144 | display: flex; 145 | padding-left: calc(var(--sidebar-left-margin) + 6px); 146 | background-color: var(--headings-background); 147 | box-sizing: border-box; 148 | border-top: 1px solid var(--code-margin); 149 | border-bottom: 1px solid var(--code-margin); 150 | } 151 | 152 | .frames-list-items { 153 | background-image: linear-gradient(var(--code-lines) 0.7px, transparent 0.7px); 154 | background-position-y: var(--cell-height); 155 | background-size: 100% var(--cell-height); 156 | height: 120px; 157 | padding-left: calc(var(--sidebar-left-margin) + 6px); 158 | overflow-y: auto; 159 | } 160 | 161 | .frames-list-item { 162 | height: var(--cell-height); 163 | display: flex; 164 | } 165 | 166 | .frames-list-item a { 167 | cursor: pointer; 168 | border-bottom: 2px solid #eee; 169 | font-family: SpaceGrotesk, sans-serif; 170 | color: var(--btn-foreground); 171 | font-size: 0.9rem; 172 | margin: auto 0 auto 0; 173 | } 174 | 175 | .make-frame-form { 176 | margin: 2px 0 auto 0; 177 | } 178 | 179 | .frame-name-input { 180 | border: 1px solid #e9e9ed; 181 | padding: 4px; 182 | } 183 | 184 | .frame-name-input:active, .frame-name-input:focus { 185 | outline: none; 186 | /* border: 2px solid #3b5aa3; */ 187 | } 188 | 189 | .code { 190 | z-index: 3; 191 | position: relative; 192 | background-color: var(--code-background); 193 | color: var(--code-foreground); 194 | background-image: linear-gradient(var(--code-lines) 0.7px, transparent 0.7px); 195 | background-position-y: var(--cell-height); 196 | background-size: 100% var(--cell-height); 197 | margin-top: -0.1px; 198 | height: 100vh; 199 | } 200 | 201 | .code-body { 202 | position: absolute; 203 | display: grid; 204 | grid-template-columns: var(--sidebar-left-margin) 1fr; 205 | top: 30px; 206 | width: 100%; 207 | height: calc(100% - 30px); 208 | } 209 | 210 | .code-text { 211 | background: none; 212 | color: var(--code-foreground); 213 | border: none; 214 | padding: 0 10px 0 10px; 215 | height: 100%; 216 | font-family: SpaceGrotesk; 217 | font-size: 0.9rem; 218 | line-height: 30px; 219 | outline: none; 220 | word-wrap: break-word; 221 | overflow-block: hidden; 222 | resize: none; 223 | } 224 | 225 | .code-thick-lines { 226 | height: calc(100% - 30px); 227 | width: 100%; 228 | background-color: transparent; 229 | background-image: linear-gradient(var(--code-lines) 0.9px, transparent 0.9px); 230 | background-size: 100% calc(var(--cell-height) * var(--code-thick-line-gap)); 231 | left: 0; 232 | margin-top: -0.2px; 233 | } 234 | 235 | .code-state-pending { 236 | background-color: var(--code-pending) !important; 237 | } 238 | 239 | .code-header { 240 | top: 0; 241 | height: var(--cell-height); 242 | width: 100%; 243 | padding: 0px; 244 | display: flex; 245 | padding-left: calc(var(--sidebar-left-margin) + 6px); 246 | padding-right: 3px; 247 | background-color: var(--headings-background); 248 | box-sizing: border-box; 249 | border-top: 1px solid var(--code-margin); 250 | } 251 | 252 | .code-error { 253 | display: inline; 254 | font-family: monospace; 255 | font-size: 9px; 256 | line-height: var(--cell-height); 257 | color: var(--sheet-error); 258 | margin-left: 10px; 259 | overflow: hidden; 260 | /* This is quite useless, but cleaner than overflowing */ 261 | } 262 | 263 | .code-header-btn { 264 | height: 24px; 265 | width: 28px; 266 | cursor: pointer; 267 | margin: auto 0px auto 0px; 268 | border: none; 269 | border-radius: 2px; 270 | color: var(--btn-foreground); 271 | background-color: var(--btn-background); 272 | } 273 | 274 | .small-btn { 275 | height: 24px; 276 | width: 28px; 277 | cursor: pointer; 278 | margin: auto 0px auto 0px; 279 | border: none; 280 | border-radius: 2px; 281 | color: var(--btn-foreground); 282 | background-color: var(--btn-background); 283 | } 284 | 285 | .code-header .dark-mode-btn { 286 | margin-left: auto; 287 | } 288 | 289 | .code-header .light-mode-btn { 290 | margin-left: auto; 291 | display: none; 292 | padding-bottom: 4px; 293 | } 294 | 295 | .code-header .help-btn { 296 | margin-left: 4px; 297 | } 298 | 299 | [data-theme='dark'] .light-mode-btn { 300 | display: block !important; 301 | } 302 | 303 | [data-theme='dark'] .dark-mode-btn { 304 | display: none; 305 | } 306 | 307 | [data-theme='light'] .dark-mode-btn { 308 | display: block !important; 309 | } 310 | 311 | .help-overlay { 312 | z-index: 10000; 313 | position: absolute; 314 | top: 0; 315 | left: 0; 316 | right: 0; 317 | bottom: 0; 318 | background: #0003; 319 | } 320 | 321 | .help-container { 322 | position: absolute; 323 | top: 15%; 324 | left: 0; 325 | right: 0; 326 | margin: auto; 327 | width: 80%; 328 | max-width: 900px; 329 | box-shadow: 0px 1px 1px 0px rgba(122, 118, 118, 0.75); 330 | border-radius: 5px; 331 | font-family: SpaceGrotesk, sans-serif; 332 | background-color: var(--sheet-background); 333 | } 334 | 335 | .help-content { 336 | margin-top: 0; 337 | box-sizing: border-box; 338 | display: grid; 339 | align-content: center; 340 | padding: 20px; 341 | padding-bottom: 35px; 342 | } 343 | 344 | .help-container h1, h3, h4, h5, p { 345 | margin-top: 5px; 346 | margin-bottom: 5px; 347 | } 348 | 349 | .help-text { 350 | display: grid; 351 | grid-template-columns: 1fr 1fr; 352 | height: 100%; 353 | } 354 | 355 | .help-open { 356 | overflow: hidden; 357 | } 358 | 359 | .help-footer { 360 | height: 30px; 361 | border-radius: 0px 0px 5px 5px; 362 | background: var(--corner-background); 363 | box-sizing: border-box; 364 | display: grid; 365 | align-content: center; 366 | padding-left: 10px; 367 | padding-right: 10px; 368 | grid-template-columns: 1fr 1fr; 369 | } 370 | 371 | .footer-p { 372 | color: grey; 373 | font-size: 0.9rem; 374 | } 375 | 376 | .footer-github-link { 377 | margin: 5px; 378 | text-align: right; 379 | font-size: 0.9rem; 380 | text-decoration: none; 381 | } 382 | 383 | [data-theme='dark'] .help-light { 384 | display: none; 385 | } 386 | 387 | [data-theme='light'] .help-dark { 388 | display: none; 389 | } 390 | 391 | .controls-container { 392 | position: relative; 393 | display: flex; 394 | height: var(--controls-height); 395 | padding-left: calc(var(--headings-left-width) + 20px); 396 | border-left: 1px solid var(--code-margin); 397 | } 398 | 399 | .controls-container::before { 400 | z-index: 4; 401 | position: absolute; 402 | content: ''; 403 | left: 33px; 404 | height: 100%; 405 | width: 1px; 406 | background: var(--code-margin); 407 | } 408 | 409 | .controls-background-buttons { 410 | margin: auto 0px auto 0px; 411 | display: flex; 412 | vertical-align: middle; 413 | } 414 | 415 | .controls-demos { 416 | position: absolute; 417 | right: 0; 418 | top: 50%; 419 | transform: translateY(-50%); 420 | } 421 | 422 | .set-background-btn { 423 | width: 15px; 424 | height: 15px; 425 | margin-right: 5px; 426 | border: 1px solid #999; 427 | border-radius: 100%; 428 | cursor: pointer; 429 | } 430 | 431 | .controls-btn { 432 | cursor: pointer; 433 | margin: auto 0px auto 0px; 434 | border: none; 435 | border-radius: 2px; 436 | color: var(--btn-foreground); 437 | padding: 0 10px 0 10px; 438 | height: 24px; 439 | margin-top: auto; 440 | margin-bottom: auto; 441 | margin-right: 20px; 442 | font-family: system-ui, -apple-system, BlinkMacSystemFont, sans-serif; 443 | } 444 | 445 | .controls-btn:disabled { 446 | cursor: default; 447 | opacity: 0.6; 448 | } 449 | 450 | .controls-btn.pressed { 451 | box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.3); 452 | } 453 | 454 | .button:active { 455 | /* Change the inset shadow to give the effect of the button being pressed */ 456 | box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.3); 457 | } 458 | 459 | .popups { 460 | position: absolute; 461 | bottom: 40px; 462 | right: 10px; 463 | } 464 | 465 | .popup { 466 | background: var(--sheet-background); 467 | transition: transform 0.5s ease; 468 | box-shadow: 0px 1px 1px 0px rgba(122, 118, 118, 0.75); 469 | border-radius: 5px; 470 | font-family: SpaceGrotesk, sans-serif; 471 | background-color: var(--sheet-background); 472 | border: 1px solid #ccc; 473 | 474 | padding: 10px; 475 | width: 400px; 476 | min-height: 70px; 477 | margin: 10px; 478 | margin-right: 20px; 479 | } 480 | 481 | .loader-popup { 482 | display: flex; 483 | } 484 | 485 | details { 486 | color: var(--btn-foreground); 487 | white-space: pre-wrap; 488 | max-height: 350px; 489 | overflow-y: scroll; 490 | } 491 | 492 | summary { 493 | cursor: pointer; 494 | } 495 | 496 | .llm-loader { 497 | animation: rotate 2s linear infinite; 498 | display: inline-block; 499 | font-size: 48px; 500 | line-height: 48px; 501 | margin: auto; 502 | color: #999999; 503 | vertical-align: middle; 504 | } 505 | 506 | @keyframes rotate { 507 | 0% { transform: rotate(0deg); } 508 | 100% { transform: rotate(360deg); } 509 | } 510 | 511 | .label-suggestion { 512 | cursor: pointer; 513 | } 514 | 515 | .label-suggestion:hover { 516 | text-decoration: underline; 517 | } 518 | -------------------------------------------------------------------------------- /public/fonts/SpaceGrotesk-Bold.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nilenso/bean/2fc6d0581e8033c95210fe9e1369c337aa488e48/public/fonts/SpaceGrotesk-Bold.png -------------------------------------------------------------------------------- /public/fonts/SpaceGrotesk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nilenso/bean/2fc6d0581e8033c95210fe9e1369c337aa488e48/public/fonts/SpaceGrotesk.png -------------------------------------------------------------------------------- /public/fonts/SpaceGrotesk.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nilenso/bean/2fc6d0581e8033c95210fe9e1369c337aa488e48/public/fonts/SpaceGrotesk.ttf -------------------------------------------------------------------------------- /public/help-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nilenso/bean/2fc6d0581e8033c95210fe9e1369c337aa488e48/public/help-dark.png -------------------------------------------------------------------------------- /public/help.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nilenso/bean/2fc6d0581e8033c95210fe9e1369c337aa488e48/public/help.png -------------------------------------------------------------------------------- /public/img/code-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nilenso/bean/2fc6d0581e8033c95210fe9e1369c337aa488e48/public/img/code-icon.png -------------------------------------------------------------------------------- /public/img/eraser.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nilenso/bean/2fc6d0581e8033c95210fe9e1369c337aa488e48/public/img/eraser.png -------------------------------------------------------------------------------- /public/img/frame-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nilenso/bean/2fc6d0581e8033c95210fe9e1369c337aa488e48/public/img/frame-icon.png -------------------------------------------------------------------------------- /public/img/left-label.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nilenso/bean/2fc6d0581e8033c95210fe9e1369c337aa488e48/public/img/left-label.png -------------------------------------------------------------------------------- /public/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nilenso/bean/2fc6d0581e8033c95210fe9e1369c337aa488e48/public/img/logo.png -------------------------------------------------------------------------------- /public/img/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /public/img/made-frame-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nilenso/bean/2fc6d0581e8033c95210fe9e1369c337aa488e48/public/img/made-frame-icon.png -------------------------------------------------------------------------------- /public/img/skip-label.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nilenso/bean/2fc6d0581e8033c95210fe9e1369c337aa488e48/public/img/skip-label.png -------------------------------------------------------------------------------- /public/img/stripes.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nilenso/bean/2fc6d0581e8033c95210fe9e1369c337aa488e48/public/img/stripes.jpg -------------------------------------------------------------------------------- /public/img/top-label.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nilenso/bean/2fc6d0581e8033c95210fe9e1369c337aa488e48/public/img/top-label.png -------------------------------------------------------------------------------- /public/img/top-left-label.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nilenso/bean/2fc6d0581e8033c95210fe9e1369c337aa488e48/public/img/top-left-label.png -------------------------------------------------------------------------------- /public/img/trash-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nilenso/bean/2fc6d0581e8033c95210fe9e1369c337aa488e48/public/img/trash-icon.png -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Bean 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /shadow-cljs.edn: -------------------------------------------------------------------------------- 1 | ;; shadow-cljs configuration 2 | {:source-paths 3 | ["src/" 4 | "test/"] 5 | 6 | :dependencies 7 | [[instaparse "1.4.12"] 8 | [bidi "2.1.6"] 9 | [kibu/pushy "0.3.8"] 10 | [reagent "1.2.0"] 11 | [re-frame "1.4.2"] 12 | [re-pressed "0.3.2"] 13 | [metosin/malli "0.13.0"] 14 | [org.clj-commons/hickory "0.7.4"] 15 | [com.github.pkpkpk/cljs-node-io "2.0.332"] 16 | [day8.re-frame/undo "0.3.3"] 17 | [com.github.flow-storm/flow-storm-inst "RELEASE"] 18 | ^:dev [day8.re-frame/re-frame-10x "1.9.3"]] 19 | 20 | :nrepl {:port 9000} 21 | :dev-http {8090 "public"} 22 | :builds 23 | {:engine {:target :node-library 24 | :output-to "output.js" 25 | :exports {:parse bean.parser.parser/parse}} 26 | :trellis {:target :node-script 27 | :main bean.trellis/main 28 | :output-to "out/trellis.js"} 29 | :ui {:target :browser 30 | :output-dir "public/js" 31 | :asset-path "/js" 32 | :devtools {:preloads [day8.re-frame-10x.preload.react-18 33 | flow-storm.preload]} 34 | :dev {:compiler-options 35 | {:closure-defines 36 | {re-frame.trace.trace-enabled? true 37 | day8.re-frame.tracing.trace-enabled? true}}} 38 | :release {:build-options 39 | {:ns-aliases 40 | {day8.re-frame.tracing day8.re-frame.tracing-stubs}}} 41 | :modules {:main {:entries [bean.ui.main] 42 | :init-fn bean.ui.main/init}}} 43 | :test {:target :node-test 44 | :output-to "tests.js" 45 | :ns-regexp "-test$"}}} 46 | -------------------------------------------------------------------------------- /src/bean/area.cljs: -------------------------------------------------------------------------------- 1 | (ns bean.area 2 | (:require [bean.util :as util])) 3 | 4 | (defn- top-left [addresses] 5 | [(apply min (map first addresses)) 6 | (apply min (map second addresses))]) 7 | 8 | (defn- bottom-right [addresses] 9 | [(apply max (map first addresses)) 10 | (apply max (map second addresses))]) 11 | 12 | (defn bounds->area [start end] 13 | {:start (top-left [start end]) 14 | :end (bottom-right [start end])}) 15 | 16 | (defn area->address-matrix [{:keys [start end]}] 17 | (util/addresses-matrix start end)) 18 | 19 | (defn area->addresses [area] 20 | (set (mapcat identity (area->address-matrix area)))) 21 | 22 | (defn addresses->area [addresses] 23 | {:start (top-left addresses) 24 | :end (bottom-right addresses)}) 25 | 26 | (defn addresses->address-matrix [addresses] 27 | (area->address-matrix (addresses->area addresses))) 28 | 29 | (defn area-empty? [{:keys [start end]}] 30 | (= start end)) 31 | 32 | (defn overlap? [area-a area-b] 33 | (let [{[a-r1 a-c1] :start [a-r2 a-c2] :end} area-a 34 | {[b-r1 b-c1] :start [b-r2 b-c2] :end} area-b] 35 | (not 36 | (or (< a-r2 b-r1) 37 | (> a-r1 b-r2) 38 | (< a-c2 b-c1) 39 | (> a-c1 b-c2))))) 40 | 41 | (defn cell-h [sheet [r c]] 42 | (let [cell (util/get-cell (:grid sheet) [r c]) 43 | [end-r _] (get-in cell [:style :merged-until])] 44 | (if end-r (inc (- end-r r)) 1))) 45 | 46 | (defn cell-w [sheet [r c]] 47 | (let [cell (util/get-cell (:grid sheet) [r c]) 48 | [_ end-c] (get-in cell [:style :merged-until])] 49 | (if end-c (inc (- end-c c)) 1))) 50 | -------------------------------------------------------------------------------- /src/bean/code.cljs: -------------------------------------------------------------------------------- 1 | (ns bean.code 2 | (:require [bean.grid :as grid])) 3 | 4 | (defn reevaluate [{:keys [code-in-editor] :as sheet}] 5 | (grid/eval-code sheet code-in-editor)) 6 | 7 | (defn set-code [sheet code] 8 | (assoc sheet :code-in-editor code)) 9 | 10 | (defn get-code [sheet] 11 | (get sheet :code-in-editor)) 12 | -------------------------------------------------------------------------------- /src/bean/code_errors.cljs: -------------------------------------------------------------------------------- 1 | (ns bean.code-errors) 2 | 3 | (defn get-error [sheet] 4 | (:code-error sheet)) 5 | 6 | (defn named-ref-error [named error] 7 | (str "name: " named ". " error)) 8 | -------------------------------------------------------------------------------- /src/bean/deps.cljs: -------------------------------------------------------------------------------- 1 | (ns bean.deps 2 | (:require [clojure.set :as set] 3 | [bean.util :as util])) 4 | 5 | (defn ->cell-dep [dep] 6 | [:cell dep]) 7 | 8 | (defn ->named-dep [dep] 9 | [:named dep]) 10 | 11 | (defn- ast->deps [ast] 12 | (let [[node-type & [arg :as args]] ast] 13 | (case node-type 14 | :CellContents (ast->deps arg) 15 | :FunctionInvocation (apply set/union 16 | (map ast->deps args)) 17 | :FunctionDefinition (ast->deps arg) 18 | :Name (if (#{"x" "y" "z"} arg) #{} #{(->named-dep arg)}) 19 | :CellRef (let [[_ a n] ast] 20 | #{(->cell-dep (util/a1->rc a (js/parseInt n)))}) 21 | :MatrixRef (->> (apply util/matrix-bounds args) 22 | (apply util/addresses-matrix) 23 | (mapcat identity) 24 | (map ->cell-dep) 25 | set) 26 | :Expression (if (util/is-expression? arg) 27 | (let [[left _ right] args] 28 | (set/union 29 | (ast->deps left) 30 | (ast->deps right))) 31 | (ast->deps arg)) 32 | #{}))) 33 | 34 | (defn- depgraph-add-edge [depgraph parent child] 35 | (assoc depgraph parent (conj (get depgraph parent #{}) child))) 36 | 37 | (defn- depgraph-remove-edge [depgraph parent child] 38 | (let [updated-dependent-set (disj (get depgraph parent) child)] 39 | (if (empty? updated-dependent-set) 40 | (dissoc depgraph parent) 41 | (assoc depgraph parent updated-dependent-set)))) 42 | 43 | (defn make-depgraph [grid] 44 | (->> grid 45 | (util/map-on-matrix-addressed 46 | #(for [dependency (ast->deps (:ast %2))] 47 | {:parent dependency :child (->cell-dep %1)})) 48 | flatten 49 | (reduce #(depgraph-add-edge %1 (:parent %2) (:child %2)) {}))) 50 | 51 | (defn update-depgraph [depgraph address old-cell new-cell] 52 | (let [old-dependencies (ast->deps (:ast old-cell)) 53 | new-dependencies (ast->deps (:ast new-cell))] 54 | (as-> depgraph g 55 | (reduce #(depgraph-remove-edge %1 %2 address) g old-dependencies) 56 | (reduce #(depgraph-add-edge %1 %2 address) g new-dependencies)))) 57 | -------------------------------------------------------------------------------- /src/bean/errors.cljs: -------------------------------------------------------------------------------- 1 | (ns bean.errors) 2 | 3 | (defn reset [value] 4 | (dissoc value :error)) 5 | 6 | (defn mark [value error] 7 | (merge value error)) 8 | 9 | (defn get-error [{:keys [error]}] 10 | error) 11 | 12 | (defn undefined-named-ref [name] 13 | {:error (str "Undefined reference: \"" name "\"") 14 | :representation (str "Undefined reference: \"" name "\"")}) 15 | 16 | (defn undefined-frame-at [address] 17 | {:error (str "No frame found at " address "") 18 | :representation (str "No frame found at " address "")}) 19 | 20 | (defn invalid-frame-args [address] 21 | {:error (str "frame() needs a cell ref, given \"" address "\"") 22 | :representation (str "frame() needs a cell ref, given \"" address "\"")}) 23 | 24 | (defn label-not-found [label-name] 25 | {:error (str "label \"" label-name "\" doesn't exist") 26 | :representation (str "label \"" label-name "\" doesn't exist")}) 27 | 28 | (defn function-not-found [] 29 | {:error "function not found" 30 | :representation "function not found"}) 31 | 32 | (defn spill-error [] 33 | {:error "Spill error" 34 | :representation "Spill error"}) 35 | 36 | (defn stringified-error 37 | "This exists to funnel all usages of automatically stringified errors 38 | centrally. We should eventually remove this and have more explicit 39 | representations for each error." 40 | [e] 41 | {:error e 42 | :representation (str e)}) 43 | 44 | (defn matrix-size-mismatch-error [] 45 | {:error "Matrices should be same size." 46 | :representation "Matrices should be same size."}) 47 | 48 | (defn divide-by-zero [] 49 | {:error "cannot divide by zero" 50 | :representation "cannot divide by zero"}) 51 | 52 | (defn type-mismatch-op [op-str] 53 | (let [e (str op-str " only works for Integers")] 54 | {:error e :representation e})) 55 | -------------------------------------------------------------------------------- /src/bean/frames.cljs: -------------------------------------------------------------------------------- 1 | (ns bean.frames 2 | (:require [bean.area :as area] 3 | [bean.util :as util] 4 | [clojure.set :as set])) 5 | 6 | (defn overlaps? [sheet frame-name area] 7 | (some 8 | #(area/overlap? % area) 9 | (vals (dissoc (:frames sheet) frame-name)))) 10 | 11 | (defn make-frame [sheet frame-name area] 12 | (if (and (not (area/area-empty? area)) 13 | (not (overlaps? sheet frame-name area))) 14 | (assoc-in sheet [:frames frame-name] 15 | (merge area {:labels {} 16 | :skip-cells #{}})) 17 | sheet)) 18 | 19 | (defn remove-frame [sheet frame-name] 20 | (update-in sheet [:frames] dissoc frame-name)) 21 | 22 | (defn cell-frame [[r c] sheet] 23 | (some 24 | (fn [[frame-name {:keys [start end]}]] 25 | (let [[start-r start-c] start 26 | [end-r end-c] end] 27 | (when (and (>= r start-r) 28 | (<= r end-r) 29 | (>= c start-c) 30 | (<= c end-c)) 31 | frame-name))) 32 | (:frames sheet))) 33 | 34 | (defn add-label [sheet frame-name rc dirn & [color]] 35 | (if (= (cell-frame rc sheet) frame-name) 36 | (assoc-in 37 | sheet 38 | [:frames frame-name :labels (util/merged-or-self rc sheet)] 39 | {:dirn dirn :color color}) 40 | sheet)) 41 | 42 | (defn add-preview-label [sheet frame-name rc dirn & [color]] 43 | (if (= (cell-frame rc sheet) frame-name) 44 | (assoc-in 45 | sheet 46 | [:frames frame-name :preview-labels (util/merged-or-self rc sheet)] 47 | {:dirn dirn :color color}) 48 | sheet)) 49 | 50 | (defn add-labels [sheet frame-name addresses dirn] 51 | (reduce #(add-label %1 frame-name %2 dirn 52 | (case dirn 53 | :top (util/random-color-hex (str (first %2) dirn)) 54 | :left (util/random-color-hex (str (second %2) dirn)) 55 | (util/random-color-hex (str %2 dirn)))) sheet addresses)) 56 | 57 | (defn add-preview-labels [sheet frame-name addresses dirn] 58 | (reduce #(add-preview-label %1 frame-name %2 dirn 0xaa0006) sheet addresses)) 59 | 60 | (defn remove-labels [sheet frame-name addresses] 61 | (reduce #(update-in % [:frames frame-name :labels] dissoc %2) sheet addresses)) 62 | 63 | (defn remove-preview-labels [sheet frame-name addresses] 64 | (reduce #(update-in % [:frames frame-name :preview-labels] dissoc %2) sheet addresses)) 65 | 66 | (defn get-frame [sheet frame-name] 67 | (get-in sheet [:frames frame-name])) 68 | 69 | (defn- get-label [sheet frame-name rc & [dirn]] 70 | (let [label (get-in sheet [:frames frame-name :labels rc])] 71 | (if dirn 72 | (when (= (:dirn label) dirn) 73 | label) 74 | label))) 75 | 76 | (defn label? [sheet frame-name label-name & [dirn]] 77 | (some 78 | (fn [[label label-data]] 79 | (and (= label-name 80 | (str (:scalar (util/get-cell (:grid sheet) label)))) 81 | (if dirn (= (:dirn label-data) dirn) true))) 82 | (:labels (get-frame sheet frame-name)))) 83 | 84 | (defn merge-labels [sheet start addresses] 85 | (if-let [frame-name (cell-frame start sheet)] 86 | (let [is-label? (get-label sheet frame-name start) 87 | other-labels? (and (not is-label?) 88 | (some #(get-label sheet frame-name %) addresses)) 89 | label (or is-label? other-labels?)] 90 | (if label 91 | (-> sheet 92 | (remove-labels frame-name addresses) 93 | (add-label frame-name start (:dirn label) (:color label))) 94 | sheet)) 95 | sheet)) 96 | 97 | (defn- last-row [[r c] sheet] 98 | (+ r (dec (area/cell-h sheet [r c])))) 99 | 100 | (defn- last-col [[r c] sheet] 101 | (+ c (dec (area/cell-w sheet [r c])))) 102 | 103 | (defn- left-blocking-label [sheet [r c] labels] 104 | (some 105 | (fn [[[r* c*] {:keys [dirn]}]] 106 | (when 107 | (and 108 | (= dirn :left) 109 | (= r r*) 110 | (= (area/cell-h sheet [r c]) 111 | (area/cell-h sheet [r* c*])) 112 | (> c* (last-col [r c] sheet))) 113 | [r* c*])) 114 | (sort-by (fn [[[_ c] _]] c) labels))) 115 | 116 | (defn- top-blocking-label [sheet [r c] labels] 117 | (some 118 | (fn [[[r* c*] {:keys [dirn]}]] 119 | (when 120 | (and 121 | (= dirn :top) 122 | (= c c*) 123 | (= (area/cell-w sheet [r c]) 124 | (area/cell-w sheet [r* c*])) 125 | (> r* (last-row [r c] sheet))) 126 | [r* c*])) 127 | (sort-by (fn [[[r _] _]] r) labels))) 128 | 129 | (defn- top-left-blocking-label [sheet [r c] labels] 130 | (or (some 131 | (fn [[[r* c*] {:keys [dirn]}]] 132 | (when (= dirn :top-left) 133 | (cond 134 | (and (= (last-row [r* c*] sheet) (last-row [r c] sheet)) 135 | (> (last-col [r* c*] sheet) (last-col [r c] sheet))) [nil (dec c*)] 136 | (and (= (last-col [r* c*] sheet) (last-col [r c] sheet)) 137 | (> (last-row [r* c*] sheet) (last-row [r c] sheet))) [(dec r*) nil] 138 | (and (> (last-row [r* c*] sheet) (last-row [r c] sheet)) 139 | (> (last-col [r* c*] sheet) (last-col [r c] sheet))) [r* c*]))) 140 | (sort-by (fn [[[r _] _]] r) (dissoc labels [r c]))) 141 | (top-blocking-label sheet [r c] labels) 142 | (left-blocking-label sheet [r c] labels))) 143 | 144 | (defn blocking-label [sheet frame-name label dirn] 145 | (let [{:keys [labels]} (get-frame sheet frame-name)] 146 | (case dirn 147 | :top (top-blocking-label sheet label labels) 148 | :top-left (top-left-blocking-label sheet label labels) 149 | :left (left-blocking-label sheet label labels)))) 150 | 151 | (defn label->cells 152 | ([sheet frame-name label] 153 | (if-let [dirn (get-in (get-frame sheet frame-name) 154 | [:labels label :dirn])] 155 | (label->cells sheet frame-name label dirn) 156 | #{})) 157 | ([sheet frame-name label dirn] 158 | (let [{:keys [end] :as frame} (get-frame sheet frame-name) 159 | [frame-end-r frame-end-c] end 160 | labels (:labels frame) 161 | merged-with-labels (mapcat 162 | #(get-in (util/get-cell (:grid sheet) %) 163 | [:style :merged-addresses]) 164 | (keys labels))] 165 | (as-> 166 | (area/area->addresses 167 | {:start label 168 | :end (let [[br bc] (blocking-label sheet frame-name label dirn)] 169 | (case dirn 170 | :top [(if br (dec br) frame-end-r) 171 | (min (last-col label sheet) frame-end-c)] 172 | :left [(min (last-row label sheet) frame-end-r) 173 | (if bc (dec bc) frame-end-c)] 174 | :top-left [(if br br frame-end-r) 175 | (if bc bc frame-end-c)]))}) cells 176 | (disj cells label) 177 | (apply disj cells merged-with-labels))))) 178 | 179 | (defn skipped-cells [sheet frame-name] 180 | (let [frame (get-frame sheet frame-name) 181 | labels (:labels frame) 182 | skip-labels (filter #(get-in frame [:skip-cells %]) (keys labels))] 183 | (set/union 184 | (set (mapcat #(label->cells sheet frame-name %) skip-labels)) 185 | (:skip-cells frame)))) 186 | 187 | (defn label-name->cells [sheet frame-name label-name & [dirn]] 188 | (let [labels (->> (keys (:labels (get-frame sheet frame-name))) 189 | (filter #(get-label sheet frame-name % dirn)) 190 | (filter #(when (= (str label-name) 191 | (str (:scalar (util/get-cell (:grid sheet) %)))) 192 | %))) 193 | skip-label? #(get-in sheet [:frames frame-name :skip-cells %]) 194 | all-skipped-cells (skipped-cells sheet frame-name) 195 | label-cells (->> labels 196 | (map #(do [% (label->cells sheet frame-name %)])) 197 | (into {})) 198 | ;; we keep track of the cells that were skipped at each step separately 199 | ;; so if a skip label is used at any step it can still access the skipped cells 200 | ;; in the function chain. 201 | skips (->> label-cells 202 | vals 203 | (apply set/union) 204 | (set/intersection all-skipped-cells))] 205 | {:cells (->> label-cells 206 | (map 207 | (fn [[label cells]] 208 | (if (skip-label? label) 209 | cells 210 | (set/difference cells skips)))) 211 | (apply set/union)) 212 | :skips skips})) 213 | 214 | (defn mark-skipped [sheet frame-name addresses] 215 | (update-in sheet [:frames frame-name :skip-cells] #(apply conj % (set addresses)))) 216 | 217 | (defn unmark-skipped [sheet frame-name addresses] 218 | (let [addresses* 219 | (set/union 220 | (set addresses) 221 | (set (mapcat #(label->cells sheet frame-name %) addresses)))] 222 | (update-in sheet [:frames frame-name :skip-cells] #(apply disj % addresses*)))) 223 | 224 | (defn- remove-outside-labels [sheet frame-name] 225 | (let [labels (get-in sheet [:frames frame-name :labels]) 226 | {:keys [start end]} (get-in sheet [:frames frame-name])] 227 | (reduce 228 | #(if-not (area/overlap? 229 | {:start start :end end} 230 | {:start %2 :end %2}) 231 | (update-in %1 [:frames frame-name :labels] dissoc %2) 232 | %1) sheet 233 | (keys labels)))) 234 | 235 | (defn resize-frame [sheet frame-name area] 236 | (when-not (overlaps? sheet frame-name area) 237 | (-> (update-in sheet [:frames frame-name] merge area) 238 | (remove-outside-labels frame-name)))) 239 | 240 | (defn- move-labels [sheet frame-name move-from move-to] 241 | (assoc-in 242 | sheet [:frames frame-name :labels] 243 | (update-keys 244 | (get-in sheet [:frames frame-name :labels]) 245 | #(util/offset move-to (util/distance move-from %))))) 246 | 247 | (defn- move-skip-cells [sheet frame-name move-from move-to] 248 | (assoc-in 249 | sheet [:frames frame-name :skip-cells] 250 | (map 251 | #(util/offset move-to (util/distance move-from %)) 252 | (get-in sheet [:frames frame-name :skip-cells])))) 253 | 254 | (defn move-frame [sheet frame-name area] 255 | (let [start (get-in sheet [:frames frame-name :start])] 256 | (-> (update-in sheet [:frames frame-name] merge area) 257 | (move-labels frame-name start (:start area)) 258 | (move-skip-cells frame-name start (:start area)) 259 | (remove-outside-labels frame-name)))) 260 | 261 | (defn expand-frames [sheet [updated-r updated-c]] 262 | (if-let [at-end-of-frame (some (fn [[frame-name {:keys [start end]}]] 263 | (when (and (= updated-r (inc (first end))) 264 | (< updated-c (inc (second end))) 265 | (>= updated-c (second start))) 266 | frame-name)) (:frames sheet))] 267 | (let [start (:start (get-frame sheet at-end-of-frame)) 268 | [end-r end-c] (:end (get-frame sheet at-end-of-frame))] 269 | (or (resize-frame sheet at-end-of-frame {:start start 270 | :end [(inc end-r) end-c]}) 271 | sheet)) 272 | sheet)) 273 | -------------------------------------------------------------------------------- /src/bean/functions.cljs: -------------------------------------------------------------------------------- 1 | (ns bean.functions 2 | (:require [bean.area :as area] 3 | [bean.errors :as errors] 4 | [bean.interpreter :as interpreter] 5 | [bean.frames :as frames] 6 | [bean.util :as util] 7 | [clojure.string])) 8 | 9 | (def blank-addr [nil nil]) 10 | (defn blank-addr? [c] (= [nil nil] c)) 11 | 12 | (defn cell-ref? [[_ ast]] 13 | (let [[expression-type] ast] 14 | (= expression-type :CellRef))) 15 | 16 | (defn quoted-string? [[_ ast]] 17 | (let [[expression-type] ast] 18 | (= expression-type :QuotedString))) 19 | 20 | ;; Functions don't work for matrices, 21 | ;; they need the thing as apply-op does 22 | (defn bean-concat [_sheet args] 23 | (interpreter/apply-results 24 | (fn [& arg] 25 | (reduce str "" arg)) 26 | args)) 27 | 28 | (defn- address-matrix->cells-matrix [sheet matrix] 29 | (util/map-on-matrix 30 | #(if (blank-addr? %) 31 | {:scalar "" :representation ""} 32 | (util/get-cell (:grid sheet) %)) 33 | matrix)) 34 | 35 | (defn- remove-nil-columns [matrix] 36 | (let [columns (apply map vector matrix) 37 | non-nil-columns (remove #(every? nil? %) columns)] 38 | (if (empty? non-nil-columns) 39 | [] 40 | (apply map vector non-nil-columns)))) 41 | 42 | (defn- remove-nil-rows [matrix] 43 | (remove #(every? nil? %) matrix)) 44 | 45 | (defn minimum-matrix [matrix] 46 | (if (zero? (count (first matrix))) 47 | [[blank-addr]] 48 | matrix)) 49 | 50 | (defn trim-blank-addrs [matrix] 51 | (let [rows (count matrix) 52 | cols (count (first matrix)) 53 | 54 | non-blanks 55 | (for [row (range rows) 56 | col (range cols) 57 | :when (not (blank-addr? (get-in matrix [row col])))] 58 | [row col]) 59 | 60 | top-row (apply min (map first non-blanks)) 61 | bottom-row (apply max (map first non-blanks)) 62 | leftmost-col (apply min (map second non-blanks)) 63 | rightmost-col (apply max (map second non-blanks))] 64 | (if top-row 65 | (->> matrix 66 | (drop top-row) 67 | (take (inc (- bottom-row top-row))) 68 | (mapv #(subvec % leftmost-col (inc rightmost-col))) 69 | minimum-matrix) 70 | (minimum-matrix [])))) 71 | 72 | (defn bean-transpose [sheet args] 73 | (if-not (:error (first args)) 74 | (let [frame-result (:frame (first args)) 75 | new-selection (apply mapv vector (:selection frame-result))] 76 | {:matrix (address-matrix->cells-matrix sheet (minimum-matrix new-selection)) 77 | :frame (merge frame-result {:selection new-selection})}) 78 | (first args))) 79 | 80 | (defn bean-row [sheet args] 81 | (if-not (:error (first args)) 82 | (let [frame-result (:frame (first args)) 83 | selection (:selection frame-result) 84 | frame (frames/get-frame sheet (:name frame-result)) 85 | [start-r start-c] (:start frame) 86 | [end-r end-c] (:end frame) 87 | cols (range start-c (inc end-c)) 88 | rows (map first (mapcat identity selection)) 89 | new-selection (minimum-matrix 90 | (for [r rows] 91 | (for [col cols] 92 | (if r [r col] blank-addr))))] 93 | {:matrix (address-matrix->cells-matrix sheet new-selection) 94 | :frame (merge frame-result {:selection new-selection})}) 95 | (first args))) 96 | 97 | (defn bean-col [sheet args] 98 | (if-not (:error (first args)) 99 | (let [frame-result (:frame (first args)) 100 | selection (:selection frame-result) 101 | frame (frames/get-frame sheet (:name frame-result)) 102 | [start-r start-c] (:start frame) 103 | [end-r end-c] (:end frame) 104 | cols (map second (mapcat identity selection)) 105 | rows (range start-r (inc end-r)) 106 | new-selection (minimum-matrix 107 | (for [r rows] 108 | (for [col cols] 109 | (if col [r col] blank-addr))))] 110 | {:matrix (address-matrix->cells-matrix sheet new-selection) 111 | :frame (merge frame-result {:selection new-selection})}) 112 | (first args))) 113 | 114 | (defn bean-reduce [sheet args] 115 | (let [frame-result (:frame (first args)) 116 | f (second args) 117 | f* #(interpreter/apply-f-args sheet f [%1 %2]) 118 | val* (first (drop 2 args)) 119 | col* (->> (:selection frame-result) 120 | (mapcat identity) 121 | sort 122 | (map #(util/get-cell (:grid sheet) %)) 123 | (remove #(clojure.string/blank? (:scalar %))))] 124 | (if val* 125 | (reduce f* val* col*) 126 | (reduce f* col*)))) 127 | 128 | ;; These don't work for matrices right now 129 | ;; It should: eval-matrix should perhaps return a :selection also 130 | (defn bean-filter [sheet args] 131 | (if (and (not (:error (first args))) 132 | (second args)) 133 | (let [frame-result (:frame (first args)) 134 | f (second args) 135 | new-selection (->> (:selection frame-result) 136 | (util/map-on-matrix 137 | #(if (:scalar 138 | (interpreter/apply-f-args 139 | sheet f [(util/get-cell (:grid sheet) %)])) 140 | % blank-addr)) 141 | minimum-matrix)] 142 | {:matrix (address-matrix->cells-matrix sheet new-selection) 143 | :frame (merge frame-result {:selection new-selection})}) 144 | (first args))) 145 | 146 | (defn bean-match [sheet args] 147 | (if-not (:error (first args)) 148 | (let [from-frame (:frame (first args)) 149 | to-frame (:frame (second args))] 150 | (when (and from-frame to-frame) 151 | (let [first-match 152 | (reduce 153 | #(if (get %1 (:representation (util/get-cell (:grid sheet) %2))) 154 | %1 155 | (assoc %1 (:representation (util/get-cell (:grid sheet) %2)) %2)) 156 | {} 157 | (mapcat identity (:selection to-frame))) 158 | 159 | new-selection 160 | (->> (util/map-on-matrix 161 | #(let [value (:representation (util/get-cell (:grid sheet) %))] 162 | (if (not-empty value) 163 | (or (get first-match value) blank-addr) 164 | blank-addr)) 165 | (:selection from-frame)))] 166 | {:matrix (address-matrix->cells-matrix sheet (minimum-matrix new-selection)) 167 | :frame {:selection new-selection 168 | :name (:name to-frame)}}))) 169 | (first args))) 170 | 171 | (defn- bean-get* [sheet args asts & [dirn]] 172 | (if-not (:error (first args)) 173 | (let [frame-result (:frame (first args)) 174 | label (:scalar (second args))] 175 | (if (frames/label? sheet (:name frame-result) label dirn) 176 | (let [label-cells (frames/label-name->cells 177 | sheet 178 | (:name frame-result) label dirn) 179 | new-selection (->> (:selection frame-result) 180 | (util/map-on-matrix 181 | #(when (or (contains? (:cells label-cells) %) 182 | (and (contains? (:skips frame-result) %) 183 | (contains? (:skips label-cells) %)) 184 | (blank-addr? %)) %)) 185 | (util/map-on-matrix #(or % blank-addr)) 186 | ;; We can't just drop blanks in a selection 187 | ;; because we want to preserve distances within the original selection, so we trim it. 188 | ;; This might point to some problems with return a set from label-name->cells 189 | ;; instead of a matrix. If we were masking a matrix with another we wouldn't 190 | ;; have to trim. Trimming can also cause slightly unexpected outputs (blanks at the end 191 | ;; get trimmed) but its alright for now. 192 | trim-blank-addrs)] 193 | {:matrix (address-matrix->cells-matrix sheet (minimum-matrix new-selection)) 194 | :frame (merge frame-result {:selection new-selection})}) 195 | (errors/label-not-found 196 | (:scalar (interpreter/eval-ast (second asts) sheet))))) 197 | (first args))) 198 | 199 | (defn bean-get [sheet args asts] 200 | (bean-get* sheet args asts)) 201 | 202 | (defn bean-vget [sheet args asts] 203 | (bean-get* sheet args asts :top)) 204 | 205 | (defn bean-hget [sheet args asts] 206 | (bean-get* sheet args asts :left)) 207 | 208 | (defn bean-frame* [sheet frame frame-name] 209 | {:matrix (interpreter/eval-matrix (:start frame) 210 | (:end frame) 211 | (:grid sheet)) 212 | :frame {:name frame-name 213 | :selection (area/area->address-matrix frame)}}) 214 | 215 | (defn bean-frame [sheet args asts] 216 | (cond 217 | (cell-ref? (first asts)) 218 | (let [[_ [_ a n]] (first asts) 219 | address (util/a1->rc a (js/parseInt n)) 220 | frame-name (frames/cell-frame address sheet) 221 | frame (frames/get-frame sheet frame-name)] 222 | (if frame-name 223 | (bean-frame* sheet frame frame-name) 224 | (errors/undefined-frame-at (str a n)))) 225 | 226 | (quoted-string? (first asts)) 227 | (let [[_ [_ frame-name]] (first asts) 228 | frame (frames/get-frame sheet frame-name)] 229 | (if frame 230 | (bean-frame* sheet frame frame-name) 231 | (errors/undefined-frame-at frame-name))) 232 | 233 | :else (errors/invalid-frame-args 234 | (str (:scalar (first args)))))) 235 | 236 | (defn bean-error [_sheet args] 237 | (let [str-err (str (:error (first args)))] 238 | {:scalar str-err 239 | :representation str-err})) 240 | -------------------------------------------------------------------------------- /src/bean/grid.cljs: -------------------------------------------------------------------------------- 1 | (ns bean.grid 2 | (:require [bean.area :as area] 3 | [bean.code-errors :as code-errors] 4 | [bean.deps :as deps] 5 | [bean.errors :as errors] 6 | [bean.functions :as functions] 7 | [bean.interpreter :as interpreter] 8 | [bean.parser.parser :as parser] 9 | [bean.parser.trellis-parser :as trellis-parser] 10 | [bean.frames :as frames] 11 | [bean.util :as util] 12 | [bean.value :as value] 13 | [clojure.set :as set] 14 | [clojure.string])) 15 | 16 | (def default-bindings 17 | {"concat" {:scalar functions/bean-concat 18 | :representation "f"} 19 | "get" {:scalar functions/bean-get 20 | :representation "f"} 21 | "vget" {:scalar functions/bean-vget 22 | :representation "f"} 23 | "hget" {:scalar functions/bean-hget 24 | :representation "f"} 25 | "frame" {:scalar functions/bean-frame 26 | :representation "f"} 27 | "filter" {:scalar functions/bean-filter 28 | :representation "f"} 29 | "reduce" {:scalar functions/bean-reduce 30 | :representation "f"} 31 | "row" {:scalar functions/bean-row 32 | :representation "f"} 33 | "col" {:scalar functions/bean-col 34 | :representation "f"} 35 | "transpose" {:scalar functions/bean-transpose 36 | :representation "f"} 37 | "match" {:scalar functions/bean-match 38 | :representation "f"} 39 | "error" {:scalar functions/bean-error 40 | :representation "f"}}) 41 | 42 | (defn- set-error [grid address error] 43 | (update-in grid 44 | address 45 | #(errors/mark % error))) 46 | 47 | (defn- clear-spilled-cell [cell spiller] 48 | (-> cell 49 | errors/reset 50 | (dissoc :scalar :spilled-from) 51 | (update :interested-spillers disj spiller) 52 | (assoc :representation ""))) 53 | 54 | (defn- empty-spilled-cell? [cell] 55 | (and (:spilled-from cell) 56 | (not (:matrix cell)) 57 | (empty? (:content cell)))) 58 | 59 | (defn- clear-spill 60 | [grid spiller] 61 | (reduce 62 | (fn [grid* addr] 63 | (if (and (empty? (get-in grid (conj addr :content))) 64 | (= (get-in grid (conj addr :spilled-from)) spiller)) 65 | (update-in grid* addr #(clear-spilled-cell % spiller)) 66 | grid*)) 67 | grid 68 | (:spilled-into (util/get-cell grid spiller)))) 69 | 70 | (defn- spill-matrix [grid spiller] 71 | (letfn 72 | [(desired-spillage 73 | [{:keys [matrix] :as cell}] 74 | "Returns a collection of cells that the given cell intends to spill" 75 | (->> matrix 76 | (util/map-on-matrix-addressed 77 | #(merge 78 | {:relative-address %1 79 | :spilled-from spiller 80 | :error (:error %2) 81 | :representation (:representation %2) 82 | :scalar (:scalar %2)} 83 | (when (= %1 [0 0]) 84 | {:matrix matrix 85 | :content (:content cell) 86 | :ast (:ast cell)}))) 87 | flatten)) 88 | 89 | (express-interests 90 | [grid* spillage] 91 | "Marks cells in a given grid for potential spillage. The potential spillage 92 | information is stored in the cell structure's `:interested-spillers` field. 93 | 94 | The :interested-spillers field is used to track spillers that need to be 95 | re-evaluated when a conflicting spiller is removed. eg. When two cells 96 | spill into a common cell and one of the cells stops spilling into the 97 | common cell, the other spiller must not have a spill error anymore and 98 | spill into the (previously common) cell successfully" 99 | (reduce 100 | #(let [{:keys [spilled-from relative-address]} %2 101 | [r c] (util/offset spilled-from relative-address) 102 | existing-spillers (get-in %1 [r c :interested-spillers] #{}) 103 | spillers* (conj existing-spillers spilled-from)] 104 | (assoc-in %1 [r c :interested-spillers] spillers*)) 105 | grid* 106 | spillage)) 107 | 108 | (spill* 109 | [grid* spillage] 110 | "Update the grid with the spillage 'applied' into the grid. Returns false 111 | if the spillage conflicts with existing content (or spillage)." 112 | (reduce 113 | #(let [{:keys [relative-address]} %2 114 | address* (util/offset spiller relative-address) 115 | cell (util/get-cell %1 address*) 116 | blank? (empty? (:content cell)) 117 | spilled-by-other? (:spilled-from cell) 118 | is-spiller? (= relative-address [0 0]) 119 | spilled-cell (merge %2 {:interested-spillers (:interested-spillers cell) 120 | :style (:style cell)})] 121 | (if (or is-spiller? (and (not spilled-by-other?) blank?)) 122 | (assoc-in %1 address* spilled-cell) 123 | (reduced false))) 124 | grid* 125 | spillage)) 126 | 127 | (spilled-addrs 128 | [spillage] 129 | (->> spillage (map #(util/offset spiller (:relative-address %))) set)) 130 | 131 | (spill 132 | [grid spillage] 133 | "If the spillage was successfully applied, sets the spiller's spilled-into. 134 | Otherwise it's marked as spill error." 135 | (if-let [grid* (spill* grid spillage)] 136 | (let [updated-addrs (spilled-addrs spillage)] 137 | [(assoc-in grid* (conj spiller :spilled-into) updated-addrs) updated-addrs]) 138 | [(set-error grid spiller (errors/spill-error)) #{spiller}]))] 139 | 140 | (let [spillage (desired-spillage (util/get-cell grid spiller))] 141 | (-> grid 142 | (express-interests spillage) 143 | (spill spillage))))) 144 | 145 | (defn parse-grid [grid] 146 | (util/map-on-matrix value/from-cell grid)) 147 | 148 | (defn eval-content [sheet content] 149 | (interpreter/eval-ast (parser/parse content) sheet)) 150 | 151 | (defn- eval-cell* [cell sheet] 152 | (if-not (empty-spilled-cell? cell) 153 | (let [parsed-cell (assoc cell :ast (parser/parse (:content cell)))] 154 | (interpreter/eval-cell parsed-cell sheet)) 155 | cell)) 156 | 157 | (defn- dependents [addrs depgraph] 158 | (->> addrs 159 | (mapcat depgraph) 160 | set)) 161 | 162 | (defn- interested-spillers [addrs grid] 163 | (->> addrs 164 | (mapcat #(get-in grid (conj % :interested-spillers))) 165 | set)) 166 | 167 | (defn new-sheet 168 | ([content-grid code] 169 | (new-sheet content-grid code "")) 170 | ([content-grid code tests] 171 | (let [parsed-grid (parse-grid content-grid)] 172 | {:grid parsed-grid 173 | :code-in-editor code 174 | :tests tests 175 | :bindings default-bindings 176 | :depgraph (deps/make-depgraph parsed-grid)}))) 177 | 178 | (defn- escalate-bindings-errors [sheet] 179 | (if (code-errors/get-error sheet) 180 | ;; This likely already set a parse error, return as is 181 | sheet 182 | (reduce (fn [sheet [named {:keys [error]}]] 183 | (if error 184 | (reduced (assoc sheet :code-error (code-errors/named-ref-error named error))) 185 | sheet)) 186 | (dissoc sheet :code-error) ;; reset the error from a previous evaluation 187 | (:bindings sheet)))) 188 | 189 | (defn- set-cell-style [sheet [r c] property value] 190 | (assoc-in sheet [:grid r c :style property] value)) 191 | 192 | (defn- unset-cell-style [sheet [r c] property] 193 | (update-in sheet [:grid r c :style] dissoc property)) 194 | 195 | (defn- get-cell-style [sheet [r c] property] 196 | (get-in sheet [:grid r c :style property])) 197 | 198 | (defn set-cell-backgrounds [sheet addresses background] 199 | (reduce #(set-cell-style %1 %2 :background background) sheet addresses)) 200 | 201 | (defn all-bold? [sheet addresses] 202 | (every? #(get-cell-style sheet % :bold) addresses)) 203 | 204 | (defn toggle-cell-bolds [sheet addresses] 205 | (if (all-bold? sheet addresses) 206 | (reduce #(unset-cell-style %1 %2 :bold) sheet addresses) 207 | (reduce #(set-cell-style %1 %2 :bold true) sheet addresses))) 208 | 209 | (declare eval-dep) 210 | 211 | (defn eval-cell 212 | ([address {:keys [grid] :as sheet}] 213 | (eval-cell address sheet (util/get-cell grid address) false)) 214 | 215 | ([address sheet new-content] 216 | (let [cell (util/get-cell (:grid sheet) address)] 217 | (eval-cell address sheet (assoc cell :content new-content) true))) 218 | 219 | ([address {:keys [grid depgraph] :as sheet} cell content-changed?] 220 | (let [cell* (eval-cell* cell sheet) 221 | clear-addrs (:spilled-into cell) 222 | cleared-grid (clear-spill grid address) 223 | unspilled-grid (assoc-in cleared-grid address cell*) 224 | [grid* evaled-addrs] (if (:matrix cell*) 225 | (spill-matrix unspilled-grid address) 226 | [unspilled-grid #{address}]) 227 | updated-addrs (set/union evaled-addrs clear-addrs) 228 | depgraph* (cond-> depgraph content-changed? 229 | (deps/update-depgraph 230 | [:cell address] cell cell*)) 231 | sheet* (merge (frames/expand-frames sheet address) 232 | {:grid grid* :depgraph depgraph*}) 233 | other-spillers (-> (interested-spillers updated-addrs grid) 234 | (disj address)) 235 | deps-to-reval (concat 236 | (dependents (map deps/->cell-dep updated-addrs) depgraph) 237 | (map deps/->cell-dep other-spillers))] 238 | (reduce #(eval-dep %2 %1) sheet* deps-to-reval)))) 239 | 240 | ;; Temporary hack until we figure out how to reval 241 | ;; frame queries 242 | (defn eval-sheet-a-few-times [sheet] 243 | (let [addresses (->> (:grid sheet) 244 | (util/map-on-matrix-addressed 245 | (fn [address item] 246 | (when (and (not= (:content item) "") 247 | (not (nil? (:content item))) 248 | ;; only reval formulas 249 | (= (first (:content item)) "=")) 250 | address))) 251 | (mapcat identity) 252 | (remove nil?))] 253 | (reduce 254 | (fn [sheet* _] (reduce #(eval-cell %2 %1) sheet* addresses)) sheet (range 3)))) 255 | 256 | (defn update-cell-content [address sheet content] 257 | (if (= (:content (util/get-cell (:grid sheet) address)) content "") 258 | sheet 259 | (eval-sheet-a-few-times (eval-cell address sheet content)))) 260 | 261 | (defn- merge-cell [sheet address merge-with] 262 | (let [sheet* (if (not= merge-with address) 263 | (eval-cell address sheet "") 264 | sheet)] 265 | (set-cell-style sheet* address :merged-with merge-with))) 266 | 267 | (defn can-merge? [sheet addresses] 268 | (let [merged-already (map #(get-cell-style sheet % :merged-with) addresses) 269 | all-merged-addresses (set 270 | (mapcat #(get-cell-style 271 | sheet 272 | (get-cell-style sheet % :merged-with) 273 | :merged-addresses) merged-already))] 274 | (and (every? #(get addresses %) all-merged-addresses) 275 | (> (count addresses) 1)))) 276 | 277 | (defn merge-cells [sheet {:keys [start end] :as area}] 278 | (let [addresses (area/area->addresses area)] 279 | (if (can-merge? sheet addresses) 280 | (-> (reduce #(merge-cell %1 %2 start) sheet addresses) 281 | (set-cell-style start :merged-until end) 282 | (set-cell-style start :merged-addresses addresses) 283 | (frames/merge-labels start addresses)) 284 | sheet))) 285 | 286 | (defn can-unmerge? [sheet addresses] 287 | (some #(get-cell-style sheet % :merged-with) addresses)) 288 | 289 | (defn unmerge-cells [sheet addresses] 290 | (->> addresses 291 | (filter #(get-cell-style sheet % :merged-with)) 292 | (reduce 293 | (fn [sheet* rc] 294 | (let [merged-with (get-cell-style sheet rc :merged-with) 295 | merged-addresses (get-cell-style sheet merged-with :merged-addresses)] 296 | (as-> sheet* sheet* 297 | (reduce #(unset-cell-style %1 %2 :merged-with) sheet* merged-addresses) 298 | (reduce #(unset-cell-style %1 %2 :merged-addresses) sheet* merged-addresses) 299 | (reduce #(unset-cell-style %1 %2 :merged-until) sheet* merged-addresses)))) 300 | sheet))) 301 | 302 | ;; untested and slightly weird interface, exists for pasting 303 | ;; many cells and handling merged cells etc. 304 | (defn update-cells-bulk [sheet {:keys [start]} addressed-attrs] 305 | (->> addressed-attrs 306 | (map #(do [(util/offset (first %) start) (second %)])) 307 | (reduce 308 | (fn [sheet* [address attrs]] 309 | (let [existing-cell (util/get-cell (:grid sheet*) address) 310 | new-cell (-> existing-cell 311 | (assoc :content (:content attrs)) 312 | (assoc :style (:style attrs))) 313 | new-sheet (eval-cell address sheet* new-cell true)] 314 | (if (:merge-until attrs) 315 | (merge-cells new-sheet 316 | {:start address 317 | :end (util/offset (:merge-until attrs) start)}) 318 | new-sheet))) 319 | (unmerge-cells sheet (map #(util/offset % start) (keys addressed-attrs)))) 320 | eval-sheet-a-few-times)) 321 | 322 | (defn make-frame [sheet frame-name addresses] 323 | (-> (frames/make-frame sheet frame-name addresses) 324 | eval-sheet-a-few-times)) 325 | 326 | (defn- move-merged-cell [cell move-by] 327 | (cond-> cell 328 | (get-in cell [:style :merged-until]) 329 | (update-in [:style :merged-until] #(util/offset % move-by)) 330 | 331 | (get-in cell [:style :merged-addresses]) 332 | (update-in [:style :merged-addresses] #(map (fn [address] (util/offset address move-by)) %)) 333 | 334 | (get-in cell [:style :merged-with]) 335 | (update-in [:style :merged-with] #(util/offset % move-by)))) 336 | 337 | (defn move-cells 338 | [sheet {:keys [start end]} move-to] 339 | (let [addresses (area/area->addresses {:start start :end end}) 340 | move-by (util/distance start move-to) 341 | empty-cell {:content "" :representation ""} 342 | cleared-sheet (reduce 343 | #(assoc-in %1 (flatten [:grid %2]) empty-cell) 344 | sheet addresses) 345 | unspilled-addresses (filter 346 | #(let [cell (util/get-cell (:grid sheet) %)] 347 | (or (:spilled-into cell) 348 | (nil? (:spilled-from cell)))) 349 | addresses)] 350 | (reduce #(assoc-in %1 351 | (flatten [:grid (util/offset move-by %2)]) 352 | (move-merged-cell (util/get-cell (:grid sheet) %2) move-by)) 353 | cleared-sheet 354 | unspilled-addresses))) 355 | 356 | (defn add-frame-labels [sheet frame-name addresses dirn] 357 | (-> (reduce #(set-cell-style %1 %2 :bold true) sheet addresses) 358 | (frames/add-labels frame-name addresses dirn) 359 | eval-sheet-a-few-times)) 360 | 361 | (defn remove-frame-labels [sheet frame-name addresses] 362 | (-> (reduce #(set-cell-style %1 %2 :bold false) sheet addresses) 363 | ;; (frames/unmark-skipped frame-name addresses) 364 | (frames/remove-labels frame-name addresses) 365 | eval-sheet-a-few-times)) 366 | 367 | (defn mark-skip-cells [sheet frame-name addresses] 368 | (-> (frames/mark-skipped sheet frame-name addresses) 369 | eval-sheet-a-few-times)) 370 | 371 | (defn unmark-skip-cells [sheet frame-name addresses] 372 | (-> (frames/unmark-skipped sheet frame-name addresses) 373 | eval-sheet-a-few-times)) 374 | 375 | (defn pasted-area [pasted-at addresses] 376 | (let [{:keys [start end]} (area/addresses->area addresses)] 377 | {:start (util/offset start pasted-at) 378 | :end (util/offset end pasted-at)})) 379 | 380 | (defn resize-frame [sheet frame-name area] 381 | (if-let [sheet* (frames/resize-frame sheet frame-name area)] 382 | (eval-sheet-a-few-times sheet*) 383 | sheet)) 384 | 385 | (defn rename-frame [sheet old-name new-name] 386 | (if-let [frame (get-in sheet [:frames old-name])] 387 | (-> sheet 388 | (update :frames dissoc old-name) 389 | (assoc-in [:frames new-name] frame) 390 | eval-sheet-a-few-times) 391 | sheet)) 392 | 393 | (defn move-frame [sheet frame-name move-to] 394 | (let [{:keys [start end]} (get-in sheet [:frames frame-name]) 395 | new-area {:start move-to :end (util/offset move-to (util/distance start end))}] 396 | ;; check for overlaps first 397 | (-> (move-cells sheet {:start start :end end} move-to) 398 | (frames/move-frame frame-name new-area) 399 | eval-sheet-a-few-times))) 400 | 401 | (defn clear-area [sheet {:keys [start end]}] 402 | (->> (util/addresses-matrix start end) 403 | (mapcat identity) 404 | (map #(do [% {:content "" :style {}}])) 405 | (into {}) 406 | (update-cells-bulk sheet start))) 407 | 408 | (defn eval-named 409 | ([name {:keys [bindings] :as sheet}] 410 | (if-let [value (bindings name)] 411 | (eval-named name sheet value) 412 | (errors/undefined-named-ref name))) 413 | 414 | ([name {:keys [bindings] :as sheet} val] 415 | (-> (let [existing-val (bindings name) 416 | val* (-> val 417 | errors/reset 418 | (merge (interpreter/eval-ast (:ast val) sheet))) 419 | deps-to-reval ((:depgraph sheet) [:named name])] 420 | (as-> sheet sheet 421 | (assoc-in sheet [:bindings name] val*) 422 | (update-in sheet [:depgraph] #(deps/update-depgraph % [:named name] existing-val val*)) 423 | (reduce #(eval-dep %2 %1) sheet deps-to-reval))) 424 | escalate-bindings-errors))) 425 | 426 | (defmulti eval-dep first) 427 | 428 | (defmethod eval-dep :cell 429 | [[_ address] sheet] 430 | (eval-cell address sheet)) 431 | 432 | (defmethod eval-dep :named 433 | [[_ name] sheet] 434 | (eval-named name sheet)) 435 | 436 | (defn- eval-grid [sheet] 437 | (util/reduce-on-sheet-addressed 438 | #(eval-cell %2 %1) 439 | sheet)) 440 | 441 | (defn eval-code 442 | ;; Suppressing errors so we let the grid evaluate before showing any errors in the code 443 | ([sheet] (eval-code sheet (:code-in-editor sheet))) 444 | ([sheet code] 445 | (let [res (let [code-ast (parser/parse-statement code) 446 | parse-error (parser/error code-ast)] 447 | (if (string? parse-error) 448 | (assoc sheet :code-error parse-error) 449 | (-> (reduce (fn [sheet [_ [_ named] expr]] 450 | (eval-named named 451 | sheet 452 | (value/from-statement (parser/statement-source code expr) 453 | expr))) 454 | (dissoc sheet :code-error) 455 | (rest code-ast)) 456 | (assoc :code-ast code-ast))))] 457 | (escalate-bindings-errors res)))) 458 | 459 | (defn eval-test [{:keys [tests] :as sheet}] 460 | (let [tests-ast (trellis-parser/parse-tests tests)] 461 | (doall 462 | (map (fn [[_ left-expr right-expr]] 463 | (let [left-val (interpreter/eval-ast left-expr sheet) 464 | right-val (interpreter/eval-ast right-expr sheet)] 465 | [(= (:scalar left-val) 466 | (:scalar right-val)) 467 | (merge {:ast left-expr 468 | :content (trellis-parser/trellis-subs tests left-expr)} 469 | left-val) 470 | (merge {:ast right-expr 471 | :content (trellis-parser/trellis-subs tests right-expr)} 472 | right-val)])) 473 | (rest tests-ast))))) 474 | 475 | (defn eval-sheet 476 | ([sheet] 477 | (->> sheet 478 | eval-code 479 | eval-grid 480 | escalate-bindings-errors))) 481 | 482 | (comment 483 | :cell 484 | {;; User input 485 | :content nil 486 | :style {:background nil} 487 | 488 | ;; Internal representation of user input 489 | :ast nil 490 | 491 | ;; Evaluation results 492 | :scalar nil 493 | :representation nil 494 | :error nil 495 | :matrix nil 496 | 497 | ;; Evaluation metadata 498 | :spilled-from nil 499 | :spilled-into nil 500 | :interested-spillers #{} 501 | 502 | ;; Addressing information 503 | :relative-address nil}) 504 | 505 | (comment 506 | :sheet 507 | {;; Source fields 508 | :grid grid 509 | :code code 510 | :frames frames 511 | 512 | ;; Evaluated fields 513 | :depgraph depgraph 514 | :bindings bindings}) 515 | -------------------------------------------------------------------------------- /src/bean/interpreter.cljs: -------------------------------------------------------------------------------- 1 | (ns bean.interpreter 2 | (:require [bean.util :as util] 3 | [bean.errors :as errors] 4 | [bean.operators :as operators])) 5 | 6 | (defn- ast-result [error-or-val] 7 | (if-let [error (:error error-or-val)] 8 | (errors/stringified-error error) 9 | {:scalar error-or-val 10 | :representation (str error-or-val)})) 11 | 12 | (defn- fn-result [f] 13 | {:scalar f 14 | :representation ""}) 15 | 16 | (defn- cell->ast-result [cell] 17 | (select-keys cell [:scalar :error :representation])) 18 | 19 | (defn- ast-result->cell [{:keys [error matrix] :as ast-result} cell] 20 | (merge 21 | {:content (:content cell) 22 | :ast (:ast cell) 23 | :scalar (:scalar ast-result) 24 | :representation (:representation ast-result)} 25 | (when matrix {:matrix matrix}) 26 | (when error {:error error}) 27 | (when (:style cell) {:style (:style cell)}))) 28 | 29 | (defn first-error [ast-results] 30 | (->> ast-results (filter :error) first)) 31 | 32 | (defn eval-matrix [start-address end-address grid] 33 | (util/map-on-matrix 34 | #(util/get-cell grid %) 35 | (util/addresses-matrix start-address end-address))) 36 | 37 | (defn apply-results 38 | ([f ast-results] 39 | (if-let [referenced-error (first-error ast-results)] 40 | referenced-error 41 | (ast-result (apply f (map :scalar ast-results))))) 42 | ([f ast-results matrix] 43 | {:matrix 44 | (util/map-on-matrix 45 | #(apply-results f (conj ast-results %)) 46 | matrix)})) 47 | 48 | (defn- dim [matrix] 49 | [(count matrix) (count (first matrix))]) 50 | 51 | (defn- matrix-op-matrix [lmatrix op rmatrix] 52 | (if (= (dim lmatrix) (dim rmatrix)) 53 | {:matrix 54 | (mapv (partial mapv 55 | (fn [l-el r-el] 56 | (apply-results 57 | #(apply %1 [%2 %3]) 58 | [op l-el r-el]))) 59 | lmatrix 60 | rmatrix)} 61 | (errors/matrix-size-mismatch-error))) 62 | 63 | (defn- scalar-op-matrix [op scalar matrix] 64 | (apply-results #(apply %1 [%2 %3]) [op scalar] matrix)) 65 | 66 | (defn- matrix-op-scalar [op scalar matrix] 67 | (apply-results #(apply %1 [%3 %2]) [op scalar] matrix)) 68 | 69 | (defn apply-op [op left right] 70 | (let [lmatrix (:matrix left) 71 | rmatrix (:matrix right)] 72 | (cond 73 | (and lmatrix rmatrix) (matrix-op-matrix lmatrix op rmatrix) 74 | ;; doesnt work for noncommutative operators 75 | lmatrix (matrix-op-scalar op right lmatrix) 76 | rmatrix (scalar-op-matrix op left rmatrix) 77 | :else (apply-results #(apply %1 [%2 %3]) [op left right])))) 78 | 79 | (declare apply-f) 80 | 81 | (defn eval-ast [ast {:keys [grid bindings] :as sheet}] 82 | ; ast goes down, result or an error comes up 83 | (let [[node-type & [arg :as args]] ast 84 | eval-sub-ast #(eval-ast % sheet) 85 | eval-matrix* #(eval-matrix %1 %2 grid)] 86 | (case node-type 87 | :CellContents (if arg 88 | (eval-sub-ast arg) 89 | (ast-result nil)) 90 | :CellRef (let [[_ a n] ast 91 | address (util/a1->rc a (js/parseInt n)) 92 | referred-cell (util/get-cell grid address)] 93 | (if (errors/get-error referred-cell) 94 | (ast-result referred-cell) 95 | (cell->ast-result referred-cell))) 96 | :MatrixRef {:matrix (->> args 97 | (apply util/matrix-bounds) 98 | (apply eval-matrix*))} 99 | :Name (or (get bindings arg) 100 | (errors/undefined-named-ref arg)) 101 | :FunctionDefinition (fn-result arg) 102 | :FunctionInvocation (apply-f sheet 103 | (eval-sub-ast arg) 104 | (rest args)) 105 | :FrameLookup (let [[_ frame-name] arg] 106 | (eval-sub-ast 107 | [:FunctionInvocation 108 | [:Name "frame"] 109 | [:Expression [:QuotedString frame-name]]])) 110 | :FunctionChain (let [[expr [node-type* 111 | [_ name* :as name-node] 112 | & fn-args]] args] 113 | (eval-sub-ast 114 | (case node-type* 115 | :LabelLookup 116 | [:FunctionInvocation 117 | [:Name "get"] expr 118 | [:Expression [:Value [:QuotedString name*]]]] 119 | :FunctionInvocation 120 | (concat [:FunctionInvocation name-node expr] fn-args)))) 121 | :Expression (if (util/is-expression? arg) 122 | (let [[left op right] args] 123 | (apply-op (eval-sub-ast op) 124 | (eval-sub-ast left) 125 | (eval-sub-ast right))) 126 | (eval-sub-ast arg)) 127 | :Value (eval-sub-ast arg) 128 | :Number (ast-result (js/Number.parseFloat arg)) 129 | :String (ast-result arg) 130 | :QuotedString (ast-result arg) 131 | :Operation (ast-result (case arg 132 | "+" operators/bean-op-+ 133 | "-" operators/bean-op-minus 134 | "/" operators/bean-op-div 135 | "*" operators/bean-op-* 136 | "<" operators/bean-op-< 137 | ">" operators/bean-op-> 138 | "=" operators/bean-op-=))))) 139 | 140 | (defn eval-asts [sheet asts] 141 | (map (fn [arg-ast] (eval-ast arg-ast sheet)) asts)) 142 | 143 | (defn- bind-to-xyz [values] 144 | (into {} (map vector ["x" "y" "z"] values))) 145 | 146 | (defn- apply-system-f [sheet f args asts] 147 | (if asts 148 | (f sheet args asts) 149 | (f sheet args))) 150 | 151 | (defn- apply-user-f [sheet f args] 152 | (eval-ast f (update-in sheet [:bindings] 153 | merge (bind-to-xyz args)))) 154 | 155 | (defn apply-f-args [sheet f args & [asts]] 156 | (let [fn-ast (:scalar f)] 157 | (cond 158 | (:error f) f 159 | (fn? fn-ast) (apply-system-f sheet fn-ast args asts) 160 | :else (apply-user-f sheet fn-ast args)))) 161 | 162 | (defn apply-f [sheet f asts] 163 | (let [args (eval-asts sheet asts)] 164 | (apply-f-args sheet f args asts))) 165 | 166 | (defn eval-cell [cell sheet] 167 | (-> (eval-ast (:ast cell) sheet) 168 | (ast-result->cell cell))) 169 | -------------------------------------------------------------------------------- /src/bean/log.cljs: -------------------------------------------------------------------------------- 1 | (ns bean.log 2 | (:require [clojure.string :as str])) 3 | 4 | (defn log [s & more] 5 | (print (str s (str/join " " more)))) -------------------------------------------------------------------------------- /src/bean/operators.cljs: -------------------------------------------------------------------------------- 1 | (ns bean.operators 2 | (:require [bean.errors :as errors])) 3 | 4 | ;; This is a hack to work around JS's float weirdnesses. 5 | (defn format-number 6 | [num] 7 | (if (.isInteger js/Number num) num (js/Number (.toFixed num 4)))) 8 | 9 | (defn bean-op-+ [left right] 10 | (if (and (number? left) (number? right)) 11 | (+ left right) 12 | (errors/type-mismatch-op "+"))) 13 | 14 | (defn bean-op-minus [left right] 15 | (if (and (number? left) (number? right)) 16 | (format-number (- left right)) 17 | (errors/type-mismatch-op "-"))) 18 | 19 | (defn bean-op-div [left right] 20 | (if (and (number? left) (number? right)) 21 | (if (zero? right) 22 | (errors/divide-by-zero) 23 | (format-number (/ left right))) 24 | (errors/type-mismatch-op "/"))) 25 | 26 | (defn bean-op-< [left right] 27 | (if (and (number? left) (number? right)) 28 | (< left right) 29 | (errors/type-mismatch-op "<"))) 30 | 31 | (defn bean-op-> [left right] 32 | (if (and (number? left) (number? right)) 33 | (> left right) 34 | (errors/type-mismatch-op ">"))) 35 | 36 | (defn bean-op-= [left right] 37 | (if (and (or (string? left) 38 | (number? left)) 39 | (or (string? right) 40 | (number? right))) 41 | (= left right) 42 | (errors/type-mismatch-op "="))) 43 | 44 | (defn bean-op-* [left right] 45 | (if (and (number? left) (number? right)) 46 | (* left right) 47 | (errors/type-mismatch-op "*"))) 48 | -------------------------------------------------------------------------------- /src/bean/parser/parser.cljs: -------------------------------------------------------------------------------- 1 | (ns bean.parser.parser 2 | (:require [instaparse.core :as insta])) 3 | 4 | (def statement-grammer 5 | " 6 | Program = Statement? { <'\n'+> Statement } 7 | = LetStatement 8 | LetStatement = Name <{' '}> <':'> <{' '}> Expression 9 | ") 10 | 11 | (def expression-grammer 12 | ;; TODO: Integers are currently just natural numbers 13 | " 14 | CellContents = <'='> Expression / RawValue / Epsilon 15 | = Number / String 16 | Number = #'[-]?[0-9]+([.][0-9]+)?' 17 | String = #'.*' 18 | 19 | CellRef = #'[A-Z]+' #'[1-9][0-9]*' 20 | MatrixRef = CellRef <':'> CellRef 21 | 22 | Operation = '+' | '*' | '=' | '<' | '>' | '/' | '-' 23 | Expression = (Value | CellRef | MatrixRef | FrameLookup | 24 | FunctionChain | Expression Operation Expression | 25 | FunctionInvocation | FunctionDefinition) / Name 26 | 27 | FunctionInvocation = (FunctionDefinition | Name) <'('> [Expression {<','> Expression}] <')'> 28 | FunctionDefinition = <'{'> Expression <'}'> 29 | FunctionChain = Expression [<'.'> (FunctionInvocation | LabelLookup)] 30 | 31 | Name = #'[a-zA-Z0-9 ]+(? Name 33 | LabelLookup = Name 34 | 35 | Value = Number / <'\"'> QuotedString <'\"'> 36 | QuotedString = #'[^\"]+' 37 | ") 38 | 39 | (def ^:private parser 40 | (insta/parser expression-grammer :auto-whitespace :standard)) 41 | 42 | (def ^:private statement-parser 43 | (insta/parser (str statement-grammer "\n" expression-grammer) :auto-whitespace :standard)) 44 | 45 | (defn parse-statement [src] 46 | (let [program (insta/parse statement-parser src)] 47 | (insta/add-line-and-column-info-to-metadata src program))) 48 | 49 | (defn parse [v] 50 | (insta/parse parser v)) 51 | 52 | (defn statement-source [code statement] 53 | (apply subs code (insta/span statement))) 54 | 55 | (defn error [result] 56 | (when (insta/get-failure result) 57 | (let [{:keys [index reason]} result] 58 | (str "Parse Error: idx " index ". " reason)))) 59 | -------------------------------------------------------------------------------- /src/bean/parser/trellis_parser.cljs: -------------------------------------------------------------------------------- 1 | (ns bean.parser.trellis-parser 2 | (:require [clojure.string :as str] 3 | [clojure.walk :as walk] 4 | [bean.parser.parser :as parser] 5 | [instaparse.core :as insta])) 6 | 7 | ;; This namespace has parsing functionality for Trellis, the cli program 8 | ;; It defines the parsing for the .leaf file format. 9 | 10 | (def ^:private test-grammer 11 | " 12 | TestProgram = Epsilon | (TestStatement { <'\n'+> TestStatement }) 13 | = AssertionStatement 14 | AssertionStatement = Expression <{' '}> <'='> <{' '}> Expression 15 | ") 16 | 17 | (def ^:private csv-grid-grammer 18 | "Note: This doesn't bother with escaping yet." 19 | 20 | " 21 | CommaSeparatedContent = CSCRow { <'\n'> CSCRow } 22 | CSCRow = CSContent <','> CSContent { <','> CSContent } 23 | = #'[^,\n]*' 24 | ") 25 | 26 | (def ^:private trellis-grammer 27 | "Note: This doesn't bother with escaping yet. Fragile & breaks with any excess '%'" 28 | 29 | " 30 | TrellisFile = Program CommaSeparatedContent TestProgram ['\n'] 31 | SectionSep = '\n'+ '%' '\n'+ 32 | ") 33 | 34 | (def ^:private trellis-parser 35 | (insta/parser (str/join "\n" [trellis-grammer 36 | test-grammer 37 | csv-grid-grammer 38 | parser/statement-grammer 39 | parser/expression-grammer]))) 40 | 41 | (defn- walk-remove-keywords [nested-structure] 42 | (walk/postwalk 43 | #(if (vector? %) 44 | (->> % 45 | (remove keyword?) 46 | (into [])) 47 | %) 48 | nested-structure)) 49 | 50 | (defn parse [content] 51 | (update-in (->> content 52 | (insta/parse trellis-parser) 53 | (insta/add-line-and-column-info-to-metadata content)) 54 | [2] ;; Remove parser keywords from the CSV content grid 55 | walk-remove-keywords)) 56 | 57 | (defn trellis-subs [src trellis-ast] 58 | (apply subs src (insta/span trellis-ast))) 59 | 60 | (def ^:private tests-parser 61 | (insta/parser (str/join "\n" [test-grammer 62 | parser/expression-grammer]))) 63 | 64 | (defn parse-tests [content] 65 | (insta/parse tests-parser content)) 66 | -------------------------------------------------------------------------------- /src/bean/provenance.cljs: -------------------------------------------------------------------------------- 1 | (ns bean.provenance 2 | (:require [bean.util :as util] 3 | [bean.interpreter :as interpreter] 4 | [bean.errors :as errors])) 5 | 6 | (defn- scalar-proof [v & args] 7 | (->> args 8 | (concat [:scalar v]) 9 | (into []))) 10 | 11 | (defn- self-evident [scalar] 12 | (scalar-proof scalar :self-evident)) 13 | 14 | (defn combine-op-proof [ast-result lproof op rproof] 15 | (let [scalar (:scalar ast-result) 16 | matrix (:matrix ast-result) 17 | lmatrix (:matrix lproof) 18 | rmatrix (:matrix rproof)] 19 | (cond 20 | (and lmatrix rmatrix) {:matrix 21 | (util/map-on-matrix-addressed 22 | #(scalar-proof (:scalar %2) (get-in lmatrix %1) op (get-in rmatrix %1)) 23 | matrix)} 24 | lmatrix {:matrix 25 | (util/map-on-matrix-addressed 26 | #(scalar-proof (get-in matrix (conj %1 :scalar)) %2 op rproof) 27 | lmatrix)} 28 | rmatrix {:matrix (util/map-on-matrix-addressed 29 | #(scalar-proof (get-in matrix (conj %1 :scalar)) lproof op %2) 30 | rmatrix)} 31 | :else (scalar-proof scalar lproof op rproof)))) 32 | 33 | (declare cell-proof) 34 | 35 | (defn ast-proof [ast sheet] 36 | ;; ast goes down, proof come up 37 | (let [[node-type & [arg :as args]] ast 38 | sub-ast-proof #(ast-proof % sheet) 39 | value #(interpreter/eval-ast ast sheet) 40 | scalar #(:scalar (value))] 41 | (case node-type 42 | :CellContents (if arg 43 | (sub-ast-proof arg) []) 44 | :CellRef (let [[_ a n] ast 45 | address (util/a1->rc a (js/parseInt n))] 46 | (cell-proof address sheet)) 47 | :MatrixRef {:matrix 48 | (->> (apply util/matrix-bounds args) 49 | (apply util/addresses-matrix) 50 | (util/map-on-matrix #(cell-proof % sheet)))} 51 | :Expression (if (util/is-expression? arg) 52 | (let [[left op right] args] 53 | (combine-op-proof 54 | (value) 55 | (sub-ast-proof left) 56 | (sub-ast-proof op) 57 | (sub-ast-proof right))) 58 | (sub-ast-proof arg)) 59 | :Value (sub-ast-proof arg) 60 | :Number (self-evident (scalar)) 61 | :String (self-evident (scalar)) 62 | :QuotedString (self-evident (scalar)) 63 | :Operation (self-evident arg)))) 64 | 65 | (defn- spilled-cell-proof [address cell {:keys [grid] :as sheet}] 66 | (let [spilled-from (util/get-cell grid (:spilled-from cell))] 67 | (conj 68 | [:spill 69 | {:spilled-from (:spilled-from cell) 70 | :content (:content spilled-from) 71 | :address address 72 | :scalar (:scalar cell) 73 | :relative-address (:relative-address cell)}] 74 | (get-in 75 | (:matrix (ast-proof (:ast spilled-from) sheet)) 76 | (:relative-address cell))))) 77 | 78 | (defn cell-proof 79 | "Returns a hiccup style proof tree. 80 | A proof is a vector of the shape [proof-type proof & dependency-proofs]. 81 | `dependency-proofs` is a list of proofs." 82 | [address {:keys [grid] :as sheet}] 83 | (let [cell (util/get-cell grid address)] 84 | (when-not (errors/get-error cell) 85 | (if (:spilled-from cell) 86 | (spilled-cell-proof address cell sheet) 87 | [:cell-ref 88 | {:address address 89 | :content (:content cell) 90 | :scalar (:scalar cell)} 91 | (ast-proof (:ast cell) sheet)])))) 92 | -------------------------------------------------------------------------------- /src/bean/trellis.cljs: -------------------------------------------------------------------------------- 1 | (ns bean.trellis 2 | (:require [bean.parser.trellis-parser :as trellis-parser] 3 | [bean.log :refer [log]] 4 | [bean.grid :as grid] 5 | [clojure.string :as str])) 6 | 7 | (def ^:private all-tests-pass (atom true)) 8 | 9 | (defn execute [filepath] 10 | (if-let [contents (str/replace (str (.readFileSync (js/require "fs") 11 | filepath)) 12 | "\r\n" ;; Replace windows newlines with linux newlines 13 | "\n")] 14 | (let [[_ code grid test] (trellis-parser/parse (str contents)) 15 | sheet (-> (grid/new-sheet grid 16 | (str 17 | (trellis-parser/trellis-subs contents code)) 18 | (str 19 | (trellis-parser/trellis-subs contents test))) 20 | grid/eval-sheet 21 | (assoc :leaf-contents contents))] 22 | (doall 23 | (->> (grid/eval-test sheet) 24 | (remove first) ;; filter test failures only 25 | (map 26 | (fn [[_ lval rval]] 27 | (reset! all-tests-pass false) 28 | (log "Assertion failed. " 29 | (:content lval) 30 | "(" 31 | (:scalar lval) 32 | ") != " 33 | (:content rval) 34 | "(" 35 | (:scalar rval) 36 | ")")))))) 37 | (log "Error loading file"))) 38 | 39 | (defn main [filepath] 40 | (log "Evaluating " filepath) 41 | (execute filepath) 42 | (when-not @all-tests-pass 43 | (.exit (js/require "process") 1))) -------------------------------------------------------------------------------- /src/bean/ui/db.cljs: -------------------------------------------------------------------------------- 1 | (ns bean.ui.db 2 | (:require [bean.grid :as grid] 3 | [bean.ui.styles :as styles])) 4 | 5 | (defn- start-sheet [num-rows num-cols] 6 | (grid/new-sheet 7 | (vec 8 | (for [_ (range num-rows)] 9 | (vec (map (fn [_] "") (range num-cols))))) 10 | "add:{x+y} 11 | inc:{x+1} 12 | sum:{x.reduce({x + y})} 13 | count:{x.reduce(inc, 0)} 14 | concatt:{x.concat(y)} 15 | average:{x.sum() / x.count()}")) 16 | 17 | (def Cell 18 | [:map 19 | [:content string?] 20 | [:ast [:vector]] 21 | [:scalar any?] 22 | [:matrix vector?] 23 | [:spilled-from [:maybe string?]] 24 | [:representation string?]]) 25 | 26 | (def AppDb 27 | [:map 28 | [:sheet [:map 29 | [:grid-dimensions [:map 30 | [:num-rows pos-int?] 31 | [:num-cols pos-int?] 32 | [:row-heights [:vector pos-int?]] 33 | [:col-widths [:vector pos-int?]]]] 34 | [:grid [:vector [:vector any?]]] 35 | [:code string?] 36 | 37 | [:depgraph any?] 38 | [:bindings [:map]] 39 | [:code-in-editor {:optional true} [:maybe string?]] 40 | [:code-error {:optional true} [:maybe string?]] 41 | [:code-ast {:optional true} [:maybe vector?]]]] 42 | [:ui [:map 43 | [:help-display boolean?] 44 | [:grid [:map 45 | [:editing-cell {:optional true}] 46 | [:selection [:vector [:map 47 | [:start nat-int?] 48 | [:end nat-int?]]]] 49 | [:selection-start [:vector nat-int?]]]]]]]) 50 | 51 | (defn initial-app-db [] 52 | (let [num-rows (:num-rows styles/sizes) 53 | num-cols (:num-cols styles/sizes)] 54 | {:sheet (-> (grid/eval-sheet (start-sheet num-rows num-cols)) 55 | (assoc :grid-dimensions {:num-rows num-rows 56 | :num-cols num-cols 57 | :row-heights (vec (repeat num-rows (:cell-h styles/sizes))) 58 | :col-widths (vec (repeat num-cols (:cell-w styles/sizes)))}) 59 | (assoc :frames {})) 60 | :ui {:help-display false 61 | :popups {} 62 | :grid {:editing-cell nil 63 | :selection nil 64 | :highlighted-cells #{}}}})) 65 | -------------------------------------------------------------------------------- /src/bean/ui/demos.cljs: -------------------------------------------------------------------------------- 1 | (ns bean.ui.demos 2 | (:require [cljs.reader :as reader])) 3 | 4 | (def file-name "demos.edn") 5 | (def db-name "bean-demos") 6 | (def object-store "bean-objects") 7 | (def demos-key "demos") 8 | 9 | (defn fetch-and-parse-edn [url] 10 | (-> (js/fetch url) 11 | (.then #(.text %)))) 12 | 13 | (defn get-demos [] 14 | (js/Promise. 15 | (fn [resolve _] 16 | (let [request (.open js/indexedDB db-name 1)] 17 | (set! (.-onsuccess request) 18 | (fn [e] 19 | (let [db (.. e -target -result) 20 | txn (.transaction db #js [object-store] "readonly") 21 | store (.objectStore txn object-store) 22 | get-request (.get store demos-key)] 23 | (set! (.-onsuccess get-request) 24 | #(resolve (reader/read-string (.. % -target -result))))))))))) 25 | 26 | (defn get-demo [demo-name] 27 | (.then (get-demos) 28 | #(get % demo-name))) 29 | 30 | (defn save-demos-locally [file] 31 | (let [request (.open js/indexedDB db-name 1)] 32 | (set! (.-onupgradeneeded request) 33 | (fn [e] 34 | (let [db (.. e -target -result)] 35 | (when (not (.contains (.-objectStoreNames db) object-store)) 36 | (.createObjectStore db object-store))))) 37 | 38 | (set! (.-onsuccess request) 39 | (fn [e] 40 | (let [db (.. e -target -result) 41 | txn (.transaction db #js [object-store] "readwrite") 42 | store (.objectStore txn object-store)] 43 | (.put store file demos-key)))))) 44 | 45 | (defn fetch-demos [] 46 | (.then (fetch-and-parse-edn (str "/" file-name)) 47 | #(save-demos-locally %))) 48 | 49 | (defn download-edn-as-file 50 | "Takes data, a filename, and an optional MIME type, and triggers a file download." 51 | [data] 52 | (let [blob-data (str data) 53 | blob (js/Blob. #js [blob-data] #js {:type "application/edn"}) 54 | url (.createObjectURL js/URL blob) 55 | link (.createElement js/document "a")] 56 | (set! (.-href link) url) 57 | (set! (.-download link) file-name) 58 | (.setAttribute link "download" file-name) 59 | (.appendChild (.-body js/document) link) 60 | (.click link) 61 | (.removeChild (.-body js/document) link) 62 | (.revokeObjectURL js/URL url))) 63 | -------------------------------------------------------------------------------- /src/bean/ui/events.cljs: -------------------------------------------------------------------------------- 1 | (ns bean.ui.events 2 | (:require [bean.code :as code] 3 | [bean.code-errors :as code-errors] 4 | [bean.frames :as frames] 5 | [bean.grid :as grid] 6 | [bean.ui.db :as db] 7 | [bean.ui.demos :as demos] 8 | [bean.ui.interceptors :refer [savepoint]] 9 | [bean.ui.llm :as llm] 10 | [bean.ui.paste :as paste] 11 | [bean.ui.provenance :as provenance] 12 | [bean.ui.util :as util] 13 | [day8.re-frame.undo :as undo :refer [undoable]] 14 | [re-frame.core :as rf] 15 | [reagent.core :as rc])) 16 | 17 | (rf/reg-event-db 18 | ::initialize-db 19 | (fn [_ [_ init-sheet]] 20 | (update-in (db/initial-app-db) [:sheet] merge init-sheet))) 21 | 22 | (rf/reg-event-db 23 | ::update-code 24 | (fn update-code [db [_ code]] 25 | (update-in db [:sheet] #(-> % 26 | (code/set-code code) 27 | (assoc :code-evaluation-state :pending))))) 28 | 29 | (rf/reg-event-db 30 | ::evaluate-code 31 | (savepoint) 32 | (fn evaluate-code [db _] 33 | (-> db 34 | (update-in [:sheet] code/reevaluate) 35 | (assoc-in [:sheet :code-evaluation-state] 36 | (if (code-errors/get-error (:sheet db)) 37 | :error 38 | :evaluated))))) 39 | 40 | (rf/reg-event-db 41 | ::reload-bindings 42 | (fn reload-bindings [db [_]] 43 | (update-in db [:sheet :bindings] merge grid/default-bindings))) 44 | 45 | (rf/reg-event-db 46 | ::update-cell 47 | [(undoable) 48 | (savepoint)] 49 | (fn update-cell [db [_ address content]] 50 | (update-in db [:sheet] #(grid/update-cell-content address % content)))) 51 | 52 | (rf/reg-event-db 53 | ::clear-area 54 | [(undoable) 55 | (savepoint)] 56 | (fn clear-area [db [_ area]] 57 | (update-in db [:sheet] #(grid/clear-area % area)))) 58 | 59 | (rf/reg-event-fx 60 | ::save-to-slot 61 | ;; for repl usage only 62 | (fn set-demo [{:keys [db]} [_ frame-name]] 63 | {:db (assoc-in db [:ui :current-demo-name] frame-name) 64 | :fx [[:dispatch [::export-demos]]]})) 65 | 66 | (rf/reg-event-fx 67 | ::export-demos 68 | (fn [{:keys [db]} [_]] 69 | (.then 70 | (demos/get-demos) 71 | #(demos/download-edn-as-file 72 | (if-let [current-demo-name (get-in db [:ui :current-demo-name])] 73 | (assoc % current-demo-name (select-keys (:sheet db) [:grid 74 | :depgraph 75 | :frames 76 | :last-frame-number 77 | :grid-dimensions 78 | :code-in-editor])) 79 | %))) 80 | {})) 81 | 82 | (rf/reg-event-fx 83 | ::reset-demos 84 | [(undoable) 85 | (savepoint)] 86 | (fn [{:keys [db]} _] 87 | ;; This is a bit of a hammer, should be able to reset better. 88 | {:fx [[:dispatch [::initialize-db]] 89 | [:dispatch [::load-demo-names (get-in db [:ui :demo-names])]]]})) 90 | 91 | (rf/reg-event-db 92 | ::load-demo 93 | [(undoable) 94 | (savepoint)] 95 | (fn [db [_ demo-name demo]] 96 | (-> db 97 | (assoc-in [:ui :current-demo-name] demo-name) 98 | (update-in [:sheet] merge demo)))) 99 | 100 | (rf/reg-event-fx 101 | ::select-demo 102 | (fn [_ [_ demo-name]] 103 | (.then (demos/get-demo demo-name) 104 | #(rf/dispatch [::load-demo demo-name %])) 105 | {})) 106 | 107 | (rf/reg-event-db 108 | ::load-demo-names 109 | (fn [db [_ demo-names]] 110 | (assoc-in db [:ui :demo-names] demo-names))) 111 | 112 | (rf/reg-event-db 113 | ::fetch-demos 114 | (fn [db []] 115 | (-> (demos/fetch-demos) 116 | (.then #(demos/get-demos)) 117 | (.then #(rf/dispatch [::load-demo-names (keys %)]))) 118 | db)) 119 | 120 | (rf/reg-event-fx 121 | ::handle-global-kbd 122 | (fn handle-global-kbd [{:keys [db]} [_ e]] 123 | (let [selection (get-in db [:ui :grid :selection])] 124 | (cond 125 | (and (= (.-key e) "z") (or (.-ctrlKey e) (.-metaKey e)) 126 | (.-shiftKey e)) 127 | (when (undo/redos?) (rf/dispatch [:redo])) 128 | 129 | (and (= (.-key e) "z") (or (.-ctrlKey e) (.-metaKey e))) 130 | (when (undo/undos?) (rf/dispatch [:undo])) 131 | 132 | (and (= (.-key e) "e") (or (.-ctrlKey e) (.-metaKey e))) 133 | {:fx [[:dispatch [::export-demos]]]} 134 | 135 | (or (.-ctrlKey e) (.-metaKey e) 136 | (= (.-key e) "Shift") 137 | (= (.-key e) "Escape")) nil 138 | 139 | :else 140 | (when-let [[r c] (:start selection)] 141 | (let [[mr mc] (get-in db [:sheet :grid r c :style :merged-until])] 142 | (if (or (= (.-key e) "Backspace") 143 | (= (.-key e) "Delete")) 144 | {:fx [[:dispatch [::clear-area selection]]]} 145 | (if-let [move-to (cond 146 | (= (.-key e) "ArrowUp") [(dec r) c] 147 | (= (.-key e) "ArrowLeft") [r (dec c)] 148 | (= (.-key e) "ArrowDown") [(if mr (inc mr) (inc r)) c] 149 | (= (.-key e) "ArrowRight") [r (if mc (inc mc) (inc c))])] 150 | {:fx [[:dispatch [::set-selection {:start move-to :end move-to}]]]} 151 | (if (= (count (.-key e)) 1) 152 | {:fx [[:dispatch [::edit-cell [r c] (.-key e)]]]} 153 | {:fx [[:dispatch [::edit-cell [r c]]]]}))))))))) 154 | 155 | (rf/reg-event-fx 156 | ::paste-addressed-cells 157 | [(undoable) 158 | (savepoint)] 159 | (fn paste-addressed-cells [{:keys [db]} [_ addressed-cells]] 160 | (let [selection (get-in db [:ui :grid :selection])] 161 | {:db (update-in db [:sheet] #(grid/update-cells-bulk % 162 | selection 163 | addressed-cells)) 164 | :fx [[:dispatch [::set-selection (grid/pasted-area 165 | (:start selection) 166 | (keys addressed-cells))]]]}))) 167 | 168 | (rf/reg-fx 169 | ::copy-to-clipboard 170 | (fn [{:keys [plain-text html]}] 171 | (.write (.-clipboard js/navigator) 172 | [(new js/ClipboardItem 173 | #js {"text/plain" (new js/Blob [plain-text] {:type "text/plain"}) 174 | "text/html" (new js/Blob [html] {:type "text/html"})})]))) 175 | 176 | (rf/reg-event-fx 177 | ::copy-selection 178 | (fn copy-selection [{:keys [db]}] 179 | (let [selection (get-in db [:ui :grid :selection])] 180 | {:fx [[::copy-to-clipboard 181 | {:plain-text (paste/selection->plain-text selection (:sheet db)) 182 | :html (paste/selection->html selection (:sheet db))}]]}))) 183 | 184 | (rf/reg-event-fx 185 | ::cut-selection 186 | [(undoable) 187 | (savepoint)] 188 | (fn cut-selection [{:keys [db]}] 189 | {:fx [[:dispatch [::copy-selection]] 190 | [:dispatch [::clear-area (get-in db [:ui :grid :selection])]]]})) 191 | 192 | (rf/reg-event-fx 193 | ::merge-cells 194 | [(undoable) 195 | (savepoint)] 196 | (fn merge-cells [{:keys [db]} [_ area]] 197 | {:db (update-in db [:sheet] #(grid/merge-cells % area)) 198 | :fx [[:dispatch [::edit-cell (:start area)]]]})) 199 | 200 | (rf/reg-event-fx 201 | ::unmerge-cells 202 | [(undoable) 203 | (savepoint)] 204 | (fn unmerge-cells [{:keys [db]} [_ addresses]] 205 | {:db (update-in db [:sheet] #(grid/unmerge-cells % addresses)) 206 | :fx [[:dispatch [::edit-cell (first addresses)]]]})) 207 | 208 | (rf/reg-event-db 209 | ::set-cell-backgrounds 210 | [(undoable) 211 | (savepoint)] 212 | (fn set-cell-backgrounds [db [_ addresses background]] 213 | (update-in db [:sheet] #(grid/set-cell-backgrounds % addresses background)))) 214 | 215 | (rf/reg-event-db 216 | ::toggle-cell-bold 217 | [(undoable) 218 | (savepoint)] 219 | (fn toggle-cell-bold [db [_ addresses]] 220 | (update-in db [:sheet] #(grid/toggle-cell-bolds % addresses)))) 221 | 222 | (rf/reg-event-fx 223 | ::submit-cell-input 224 | (fn submit-cell-input [{:keys [db]} [_ content]] 225 | (let [editing-cell (get-in db [:ui :grid :editing-cell])] 226 | {:fx [[:dispatch [::clear-edit-cell]] 227 | [:dispatch [::update-cell editing-cell content]]]}))) 228 | 229 | (rf/reg-event-db 230 | ::resize-row 231 | [(undoable) 232 | (savepoint)] 233 | (fn resize-row [db [_ row height]] 234 | (assoc-in db [:sheet :grid-dimensions :row-heights row] height))) 235 | 236 | (rf/reg-event-db 237 | ::resize-col 238 | [(undoable) 239 | (savepoint)] 240 | (fn resize-col [db [_ col width]] 241 | (assoc-in db [:sheet :grid-dimensions :col-widths col] width))) 242 | 243 | (rf/reg-fx 244 | ::focus-element 245 | (fn [[el-id text]] 246 | (rc/after-render 247 | #(when-let [el (-> js/document (.getElementById el-id))] 248 | (set! (.-innerHTML el) (or text "")) 249 | (.focus el) 250 | (.selectAllChildren (.getSelection js/window) el) 251 | (.collapseToEnd (.getSelection js/window)))))) 252 | 253 | (rf/reg-event-fx 254 | ::edit-cell 255 | (fn edit-cell [{:keys [db]} [_ rc text]] 256 | (let [rc* (util/merged-or-self rc (:sheet db))] 257 | (when (get-in db (flatten [:sheet :grid rc*])) 258 | (let [content (get-in db (flatten [:sheet :grid rc* :content]))] 259 | {:db (assoc-in db [:ui :grid :editing-cell] rc*) 260 | :fx [[:dispatch [::set-selection {:start rc* :end (util/merged-until-or-self rc* (:sheet db))}]] 261 | [::focus-element ["cell-input" (or text content)]]]}))))) 262 | 263 | (rf/reg-event-db 264 | ::clear-edit-cell 265 | (fn clear-edit-cell [db [_]] 266 | (assoc-in db [:ui :grid :editing-cell] nil))) 267 | 268 | (rf/reg-event-db 269 | ::set-selection 270 | (fn [db [_ selection]] 271 | (when (util/area-inside? (:sheet db) selection) 272 | (assoc-in db [:ui :grid :selection] selection)))) 273 | 274 | (rf/reg-event-db 275 | ::clear-selection 276 | (fn [db [_]] 277 | (assoc-in db [:ui :grid :selection] nil))) 278 | 279 | (rf/reg-event-fx 280 | ::make-frame 281 | [(undoable) 282 | (savepoint)] 283 | (fn make-frame [{:keys [db]} [_ area]] 284 | (let [frame-number (inc (get-in db [:sheet :last-frame-number])) 285 | frame-name (str "Frame " frame-number)] 286 | {:db (-> db 287 | (assoc-in [:sheet :last-frame-number] frame-number) 288 | (update-in [:sheet] #(grid/make-frame % frame-name area))) 289 | :fx [[:dispatch [::select-frame frame-name]]]}))) 290 | 291 | (rf/reg-event-fx 292 | ::remove-frame 293 | [(undoable) 294 | (savepoint)] 295 | (fn remove-frame [{:keys [db]} [_ frame-name]] 296 | {:db (update-in db [:sheet] #(frames/remove-frame % frame-name))})) 297 | 298 | (rf/reg-event-fx 299 | ::select-frame 300 | (fn select-table [{:keys [db]} [_ frame-name]] 301 | (let [area (get-in db [:sheet :frames frame-name])] 302 | (when area {:fx [[:dispatch [::set-selection area]]]})))) 303 | 304 | (rf/reg-event-fx 305 | ::renaming-frame 306 | (fn renaming-frame [{:keys [db]} [_ frame-name]] 307 | {:db (assoc-in db [:ui :renaming-frame] frame-name) 308 | :fx [[:dispatch [::select-frame frame-name]]]})) 309 | 310 | (rf/reg-event-db 311 | ::highlight-matrix 312 | (fn highlight-matrix [db [_ content]] 313 | (assoc-in db [:ui :grid :highlighted-cells] 314 | (set (mapcat identity (get-in 315 | (grid/eval-content (:sheet db) content) 316 | [:frame :selection])))))) 317 | 318 | (rf/reg-event-db 319 | ::rename-frame 320 | [(undoable) 321 | (savepoint)] 322 | (fn edit-frame [db [_ old-name new-name]] 323 | (update db :sheet #(grid/rename-frame % old-name new-name)))) 324 | 325 | (rf/reg-event-db 326 | ::add-labels 327 | [(undoable) 328 | (savepoint)] 329 | (fn add-labels [db [_ frame-name addresses dirn]] 330 | (update-in db [:sheet] 331 | #(grid/add-frame-labels % frame-name addresses dirn)))) 332 | 333 | (rf/reg-event-db 334 | ::add-preview-labels 335 | (fn add-preview-labels [db [_ frame-name addresses dirn]] 336 | (update-in db [:sheet] 337 | #(frames/add-preview-labels % frame-name addresses dirn)))) 338 | 339 | (rf/reg-event-db 340 | ::remove-labels 341 | [(undoable) 342 | (savepoint)] 343 | (fn remove-labels [db [_ frame-name addresses]] 344 | (update-in db [:sheet] #(grid/remove-frame-labels % frame-name addresses)))) 345 | 346 | (rf/reg-event-db 347 | ::remove-preview-labels 348 | (fn remove-preview-labels [db [_ frame-name addresses]] 349 | (update-in db [:sheet] #(frames/remove-preview-labels % frame-name addresses)))) 350 | 351 | (rf/reg-event-db 352 | ::dismiss-popup 353 | (fn dismiss-popup [db [_ popup-type]] 354 | (update-in db [:ui :popups] dissoc popup-type))) 355 | 356 | (rf/reg-event-db 357 | ::popup-add-labels 358 | (fn popup-add-labels [db [_ suggestions]] 359 | (-> (assoc-in db [:ui :asking-llm] false) 360 | (update-in [:ui :popups] assoc :add-labels suggestions)))) 361 | 362 | (rf/reg-event-fx 363 | ::ask-labels-llm 364 | (fn add-labels-llm [{:keys [db]} [_ frame-name addresses]] 365 | (.then (llm/suggest-labels (:sheet db) frame-name) 366 | #(rf/dispatch [::popup-add-labels %])) 367 | {:db (assoc-in db [:ui :asking-llm] true) 368 | :fx [[:dispatch [::dismiss-popup :add-labels]]]})) 369 | 370 | (rf/reg-event-db 371 | ::mark-skip-cells 372 | [(undoable) 373 | (savepoint)] 374 | (fn mark-skip-cells [db [_ frame-name addresses]] 375 | (update-in db [:sheet] 376 | #(grid/mark-skip-cells % frame-name addresses)))) 377 | 378 | (rf/reg-event-db 379 | ::unmark-skip-cells 380 | [(undoable) 381 | (savepoint)] 382 | (fn unmark-skip-cells [db [_ frame-name addresses]] 383 | (update-in db [:sheet] 384 | #(grid/unmark-skip-cells % frame-name addresses)))) 385 | 386 | (rf/reg-event-db 387 | ::explain 388 | (fn explain [db [_ expression]] 389 | (assoc-in db 390 | [:ui :provenance] 391 | (provenance/sentence-proof expression (:sheet db))))) 392 | 393 | (rf/reg-event-db 394 | ::resize-frame 395 | [(undoable) 396 | (savepoint)] 397 | (fn resize-frame [db [_ frame-name end]] 398 | (let [start (get-in (:sheet db) [:frames frame-name :start])] 399 | (when (and (not= (get-in (:sheet db) [:frames frame-name :end]) end) 400 | (>= (first end) (first start)) 401 | (>= (second end) (second start))) 402 | (update-in db [:sheet] 403 | #(grid/resize-frame % frame-name {:start start :end end})))))) 404 | 405 | (rf/reg-event-db 406 | ::move-frame 407 | [(undoable) 408 | (savepoint)] 409 | (fn move-frame [db [_ frame-name move-to]] 410 | (update-in db [:sheet] #(grid/move-frame % frame-name move-to)))) 411 | 412 | (rf/reg-event-db 413 | ::display-help 414 | (fn display-help [db [_ flag]] 415 | (assoc-in db [:ui :help-display] flag))) 416 | 417 | (rf/reg-event-db 418 | ::set-route 419 | (fn set-route [db [_ match]] 420 | (assoc-in db [:route] match))) 421 | 422 | (rf/reg-event-db 423 | ::set-anthropic-api-key 424 | [(savepoint)] 425 | (fn [db [_ api-key]] 426 | (assoc-in db [:sheet :anthropic-api-key] api-key))) 427 | -------------------------------------------------------------------------------- /src/bean/ui/features.cljs: -------------------------------------------------------------------------------- 1 | (ns bean.ui.features 2 | (:require [clojure.string :as string])) 3 | 4 | (def show-control-bar true) 5 | (defn llm-labelling? [sheet] 6 | (not (clojure.string/blank? (get-in sheet [:anthropic-api-key])))) 7 | -------------------------------------------------------------------------------- /src/bean/ui/interceptors.cljs: -------------------------------------------------------------------------------- 1 | (ns bean.ui.interceptors 2 | (:require [bean.ui.save :as save] 3 | [re-frame.core :as re-frame])) 4 | 5 | (defn savepoint [] 6 | (re-frame/->interceptor 7 | :id :savepoint 8 | :after (fn [context] 9 | (let [{:keys [db]} (:effects context)] 10 | (save/write-sheet (:sheet db))) 11 | context))) 12 | -------------------------------------------------------------------------------- /src/bean/ui/llm.cljs: -------------------------------------------------------------------------------- 1 | (ns bean.ui.llm 2 | (:require [bean.util :as util] 3 | [clojure.set :as set] 4 | [clojure.string :as string] 5 | [hickory.render :as hr] 6 | [cljs.reader :refer [read-string]])) 7 | 8 | (def system-prompt 9 | "You are annotating different parts of a table structure using this ruleset.\n- A label is any cell that categorises, points to or indexes data in the table \n(these can be headers, section headers, row headers, summary headers or other descriptors).\n- Labels have a \"direction\":\n t2b (top-to-bottom) if the data is below it, \n l2r (left-to-right) if the data is on it's right,\n tl2br (top-left-to-bottom-right) if the data is diagonally aligned. \ntl2br labels point to data on their bottom right. tl2br labels don't span the entire range they point to. These need most attention while annotating.\n- Summaries, aggregates, units, text in parentheses/square brackets, notes and marginalia are all considered 'skipped' cells.\n\nExample 1\nShows a simple structure.\n
202020212022
Revenue748019890169080
Expenses545224202431429
Profit
\nResponse:\n[21 2] [21 3] [21 4] are t2b labels\n[22 1] [23 1] [24 1] are l2r labels\n{[21 2] :t2b [21 3] :t2b [21 4] :t2b [22 1] :l2r [23 1] :l2r [24 1] :l2r}\n#{}\n\nExample 2\nShows a table with summaries in between and a mixture of labels.\n
Employee DataNameCountrySalaryTax Amount
EngineeringJohnSingapore24498
TanmayIndia10082
JamesFinland78217
Total13424
SalesSujanIndia26560
SamsonFinland50393
NoteSalary is Pre-Tax
\nResponse:\n[3 2] [3 3] [3 4] [3 5] are t2b labels\n[4 1] [7 3] [8 1] are l2r labels\n[3 1] is a tl2br label since it points to the entire table but doesn't have a span\n[12 3] [7 3] are skip cells\n{[4 1] :l2r [3 2] :t2b [3 3] :t2b [3 4] :t2b [7 1] :l2r [7 3] :l2r [3 1] :tl2br}\n#{[12 3] [7 3]}\n\nExample 3\nAn example of top left labels used to create sections.\n
CityPopulation
SouthBangalore1200
Pondicherry950000
Kochi2100000
Chennai7100000
NorthMumbai12500000
Delhi18900000
Chandigarh1100000
\nResponse:\n[47 9] [47 10] are t2b labels\n[48 8] [52 8] are tl2br labels\n{[47 9] :t2b [47 10] :t2b [48 8] :tl2br [52 8] :tl2br}\n#{}\n\nExample 4\nAn example of labels that have the same direction and span creating sections (Q1 and Q2).\n
RevenueExpensesProfit
Q1
1314421272
1314421272
1314421272
1314421272
Q2
1314421272
1314421272
1314421272
1314421272
\nResponse:\n[61 6] [61 7] [61 8] [62 6] [67 6] are t2b labels\n{[61 6] :t2b [61 7] :t2b [61 8] :t2b [62 6] :t2b [67 6] :t2b}\n#{}\n\nExample 6\nAn example of serial number/units for a label that should to be skipped.\n
200220062010
(1)(2)cm
21.2%17.5%17.4%
\n[18 5] [18 6] [18 7] are t2b labels\n[19 5] [19 6] [19 7] are skip cells\n[19 5] [19 6] [19 7] are not labels\n{[18 5] :t2b [18 6] :t2b [18 7] :t2b }\n#{[19 5] [19 6] [19 7]}\n\nExample 7\nAn example of tl2br label.\n
Percent of employees covered
Own company stock21.2%
Stock options13.1%
\n[38 6] is a tl2br label since it describes the section on it's bottom right but it's not a merged cell\n[39 7] [40 7] are t2b labels\n{[38 6] :tl2br [39 7] :l2r [40 7] :l2r}\n#{}\n\nExample 8\nAn example similar to example 4.\n
Product: Conv-Conforming Fixed Rate 30 Year
Rate15 Day30 Day45 Day60 Day
4102.125102101.875101.5
3.875101.297101.172101.047100.672
Max102.125102101.875101.5
\nResponse:\n[10 1] [11 1] [11 2] [11 3] [11 4] are t2b labels\n[14 1] is a l2r label\n[14 1] is also a skip label\nNo tl2br labels\n{[10 1] :t2b [11 1] :t2b [11 2] :t2b [11 3] :t2b [11 4] :t2b [14 1] :l2r}\n#{[14 1]}\n\nExample 9\n
DateQLEIXQLENXBenchmark*
July-130.60%0.60%0.26%
August-13-2.88%-2.88%-1.06%
September-134.50%4.50%2.48%
October-134.31%4.21%1.95%
November-132.82%2.82%0.89%
December-131.52%1.50%1.07%
January-14-3.04%-3.04%-1.86%
\nResponse:\nWhile users may often think of the leftmost column as the \"primary key\" or identifier for the data rows, in this case, the dates are not serving that purpose. \n{[61 2] :t2b [61 3] :t2b [61 4] :t2b}\n#{}\n\nFind the labels in the given table. You don't care about blank cells. First explain your reasoning in . At the end include only the addresses and their directions in and skipped cells in .") 10 | 11 | (defn call-llm-server [prompt api-key] 12 | (-> (js/fetch "https://prabhanshu-claudeforwarder.web.val.run" 13 | (clj->js 14 | {:method "POST" 15 | :headers {"Content-Type" "application/json" 16 | "x-api-key" api-key} 17 | :body (js/JSON.stringify 18 | #js {:user_prompt prompt 19 | :system_prompt system-prompt})})) 20 | (.then #(.text %)))) 21 | 22 | (defn hiccup-matrix->html [matrix] 23 | (hr/hiccup-to-html 24 | [[:table {} 25 | (into [:tbody {}] matrix)]])) 26 | 27 | (defn merged-with-another? [cell] 28 | (and (get-in cell [:style :merged-with]) 29 | (not (get-in cell [:style :merged-until])))) 30 | 31 | (defn cell->hiccup-cell [cell [r c]] 32 | (let [[mr mc] (get-in cell [:style :merged-until])] 33 | (when-not (merged-with-another? cell) 34 | [:td 35 | (merge 36 | {:data-addr [r c]} 37 | (when-not (empty? (:style cell)) 38 | {:style (string/join ";" 39 | [(when (get-in cell [:style :bold]) 40 | "font-weight: bold") 41 | (when-let [bg (get-in cell [:style :background])] 42 | (str "background: " (.toString bg 16)))])}) 43 | (when mc {:colspan (str (inc (- mc c)))}) 44 | (when mr {:rowspan (str (inc (- mr r)))})) 45 | (:representation cell)]))) 46 | 47 | (defn frame->html 48 | [{:keys [start end]} sheet] 49 | (->> (util/addresses-matrix start end) 50 | (util/map-on-matrix #(cell->hiccup-cell (get-in (:grid sheet) %) %)) 51 | (map #(into [] (remove nil? (into [:tr {}] %)))) 52 | hiccup-matrix->html)) 53 | 54 | (def llm-dirn->bean-dirn 55 | {:t2b :top 56 | :l2r :left 57 | :tl2br :top-left}) 58 | 59 | (defn extract-tag-from-text [tag s] 60 | (second (re-find (re-pattern (str "(?s)<" tag ">(.*?)")) s))) 61 | 62 | (defn suggest-labels [sheet frame-name] 63 | (let [frame (get-in sheet [:frames frame-name]) 64 | frame-html (frame->html frame sheet) 65 | prompt frame-html 66 | api-key (:anthropic-api-key sheet)] 67 | (-> (call-llm-server prompt api-key) 68 | (.then 69 | #(let [label-dirns (->> (extract-tag-from-text "labels" %) 70 | read-string 71 | (reduce (fn [mapped-dirns [k v]] 72 | (assoc mapped-dirns k (get llm-dirn->bean-dirn v))) 73 | {})) 74 | skip-cells (-> (extract-tag-from-text "skip-cells" %) read-string)] 75 | {:frame-name frame-name 76 | :labels label-dirns 77 | :skip-cells skip-cells 78 | :explanation %}))))) 79 | 80 | 81 | -------------------------------------------------------------------------------- /src/bean/ui/main.cljs: -------------------------------------------------------------------------------- 1 | (ns bean.ui.main 2 | (:require [bean.ui.events :as events] 3 | [bean.ui.views.sheet :as sheet] 4 | [bean.ui.routes :as routes] 5 | [bean.ui.views.root :as root] 6 | [bean.ui.save :as save] 7 | [re-frame.core :as rf] 8 | [reagent.dom :as r])) 9 | 10 | (defn ^:dev/after-load main* [] 11 | (routes/start) 12 | (r/render 13 | [root/routed] 14 | (.getElementById js/document "app")) 15 | (rf/dispatch [::events/reload-bindings])) 16 | 17 | (defn init [] 18 | (.addEventListener js/window "keydown" (fn [e] (sheet/handle-global-kbd e))) 19 | (.addEventListener js/window "paste" (fn [e] (sheet/handle-paste e))) 20 | (.addEventListener js/window "copy" (fn [e] (sheet/handle-copy e))) 21 | (.addEventListener js/window "cut" (fn [e] (sheet/handle-cut e)))) 22 | 23 | (defn ^:export main [] 24 | (-> (save/read-sheet) 25 | (.then #(do (rf/dispatch-sync [::events/initialize-db %]) 26 | (rf/dispatch [::events/fetch-demos]) 27 | (main*))))) 28 | -------------------------------------------------------------------------------- /src/bean/ui/paste.cljs: -------------------------------------------------------------------------------- 1 | (ns bean.ui.paste 2 | (:require [bean.ui.util :as util] 3 | [clojure.string :as string] 4 | [hickory.core :as hickory] 5 | [hickory.render :as hr] 6 | [hickory.convert :as hc] 7 | [hickory.select :as hs])) 8 | 9 | (def sample "\n\n\n\n\n\n\n\n\n\n\n
\n\n\n\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n
Season\n nameEpisodes
Last aired
Indigo League82
Adventures in the Orange\n Islands36
\n\n\n\n\n") 10 | (def sample2 "
January 28, 1999October 7, 1999
3The Johto Journeys41October 14, 1999July 27, 2000
4Johto League Champions52August 3, 2000August 2, 2001
5Master Quest65August 9, 2001November 14, 2002
6Advanced40November 21, 2002August 28, 2003
7Advanced Challenge52September 4, 2003September 2, 2004
8Advanced Battle53September 9, 2004September 29, 2005
9Battle Frontier47October 6, 2005Septembe
") 11 | (def sample3 "
fwefwe
fwefwef
fwefwe
") 12 | (def sample4 "
September 4, 2003September 2, 2004
8Advanced Battle53September 9, 2004September 29, 2005
9Battle FrontierBattle Frontierfwfwefwef47October 6, 2005September 14, 2006
10Diamond and Pearl52September 28, 2006October 25, 2007
") 13 | 14 | (defn css-to-map [css-str] 15 | (->> (string/split css-str #";") 16 | (map string/trim) 17 | (map #(map string/trim (string/split % #":"))) 18 | (map (fn [[k v]] [(keyword k) v])) 19 | (into {}))) 20 | 21 | (defn inner-text [hiccup-form] 22 | (-> (map 23 | #(cond 24 | (vector? %) (inner-text %) 25 | (string? %) % 26 | :else "") 27 | hiccup-form) 28 | string/join 29 | ;; Dealing with some weird unicode whitespace 30 | ;; that some of the excel sheets had. 31 | ;; They break rendering and in some cases the parser. 32 | (string/replace 33 | #"\u000C|\u000D|\u0020|\u0085|\u00A0|\u1680|\u2000|\u2001|\u2002|\u2003|\u2004|\u2005|\u2006|\u2007|\u2008|\u2009|\u200A|\u2028|\u2029|\u202F|\u205F|3000|\n" 34 | " ") 35 | (string/replace " " " ") 36 | ;; Fixing special characters escaped by hickory so they're displayed normally. 37 | ;; There should be a better way to not url-encode strings in the first place. 38 | (string/replace "&" "&") 39 | (string/replace " " " ") 40 | (string/replace "<" "<") 41 | (string/replace ">" ">") 42 | (string/replace """ "\""))) 43 | 44 | (defn hiccup-cell->cell [hiccup-cell merge-until] 45 | (let [style (css-to-map (:style (second hiccup-cell)))] 46 | {:content (or 47 | (:data-bean-content (second hiccup-cell)) 48 | (inner-text hiccup-cell) "") 49 | :style (merge 50 | (when (or (= (first hiccup-cell) :th) 51 | (= (:font-weight style) "bold")) 52 | {:bold true}) 53 | (when-let [bg (:background style)] {:background bg})) 54 | ;; this is a weird place to sneak "merge"-ing cells but 55 | ;; passing it in from here for now to the grid so I can do everything in the 56 | ;; in the same function. 57 | :merge-until merge-until})) 58 | 59 | (defn merged-with-another? [cell] 60 | (and (get-in cell [:style :merged-with]) 61 | (not (get-in cell [:style :merged-until])))) 62 | 63 | (defn cell->hiccup-cell [cell [r c]] 64 | (let [[mr mc] (get-in cell [:style :merged-until])] 65 | (when-not (merged-with-another? cell) 66 | [:td 67 | (merge 68 | {:data-bean-content (:content cell) 69 | :style (string/join ";" 70 | [(when (get-in cell [:style :bold]) 71 | "font-weight: bold") 72 | (when-let [bg (get-in cell [:style :background])] 73 | (str "background: " (.toString bg 16)))])} 74 | (when mc {:colspan (str (inc (- mc c)))}) 75 | (when mr {:rowspan (str (inc (- mr r)))})) 76 | (:representation cell)]))) 77 | 78 | (defn hiccup-matrix->html [matrix] 79 | (hr/hiccup-to-html 80 | [[:table {} 81 | (into [:tbody {}] matrix)]])) 82 | 83 | (defn hickory-table->cells [hickory-table] 84 | (loop [hiccup-cells (->> 85 | (hs/select (hs/child (hs/tag :tr)) hickory-table) 86 | (mapv #(hs/select (hs/child (hs/or (hs/tag :td) (hs/tag :th))) %)) 87 | (util/map-on-matrix-addressed (fn [idx cell] [idx (hc/hickory-to-hiccup cell)])) 88 | (mapcat identity) 89 | (sort-by first)) 90 | occupieds #{} 91 | cells {}] 92 | (let [[idx hiccup-cell] (first hiccup-cells) 93 | [_ {:keys [colspan rowspan]}] hiccup-cell 94 | rowspan (and rowspan (js/parseInt rowspan)) 95 | colspan (and colspan (js/parseInt colspan)) 96 | [r c] (loop [idx* idx] 97 | (if (get occupieds idx*) 98 | (recur [(first idx*) (inc (second idx*))]) 99 | idx*)) 100 | merge-until [(+ r (if (pos-int? rowspan) (dec rowspan) 0)) 101 | (+ c (if (pos-int? colspan) (dec colspan) 0))] 102 | got-occupied (mapcat identity (util/addresses-matrix [r c] merge-until))] 103 | (if (empty? hiccup-cells) 104 | cells 105 | (recur 106 | (rest hiccup-cells) 107 | (into occupieds got-occupied) 108 | (assoc cells [r c] (hiccup-cell->cell hiccup-cell (when (not= merge-until [r c]) merge-until)))))))) 109 | 110 | (defn plain-text->cells [text] 111 | (->> (string/split text "\n") 112 | (map #(string/split % "\t")) 113 | (map (partial map #(do {:content %}))) 114 | (util/map-on-matrix-addressed (fn [idx cell] [idx cell])) 115 | (mapcat identity) 116 | (into {}))) 117 | 118 | (defn text->hickory-table [pasted-text] 119 | (->> pasted-text 120 | hickory/parse-fragment 121 | (map hickory/as-hickory) 122 | (map #(hs/select (hs/tag "table") %)) 123 | (some not-empty) 124 | first)) 125 | 126 | (defn parse-table [e] 127 | (when-let [table (text->hickory-table 128 | (.getData (.-clipboardData e) "text/html"))] 129 | (hickory-table->cells table))) 130 | 131 | (defn parse-plaintext [e] 132 | (plain-text->cells 133 | (.getData (.-clipboardData e) "text"))) 134 | 135 | (defn selection->html 136 | [{:keys [start end]} sheet] 137 | (->> (util/addresses-matrix start end) 138 | (util/map-on-matrix #(cell->hiccup-cell (get-in (:grid sheet) %) %)) 139 | (map #(into [] (remove nil? (into [:tr {}] %)))) 140 | hiccup-matrix->html)) 141 | 142 | (defn selection->plain-text 143 | [{:keys [start end]} sheet] 144 | (->> (util/addresses-matrix start end) 145 | (util/map-on-matrix #(get-in (:grid sheet) (conj % :content))) 146 | (map #(string/join "\t" %)) 147 | (string/join "\n"))) 148 | -------------------------------------------------------------------------------- /src/bean/ui/provenance.cljs: -------------------------------------------------------------------------------- 1 | (ns bean.ui.provenance 2 | (:require [bean.ui.util :as util] 3 | [bean.provenance :as provenance] 4 | [bean.parser.parser :as parser] 5 | [clojure.string :as string])) 6 | 7 | (defn subtree-proof-sentence 8 | [spacer [_ proof & dependency-proofs]] 9 | (let [[res que] 10 | (reduce 11 | (fn [[s queue] subproof] 12 | (let [[proof-type p & rest] subproof] 13 | [(str s (case proof-type 14 | :scalar (str p " ") 15 | :spill (str (apply util/rc->a1 (:address p)) " ") 16 | :cell-ref (str (apply util/rc->a1 (:address p)) " "))) 17 | (if (coll? (first rest)) 18 | (conj queue subproof) 19 | queue)])) 20 | (case _ 21 | :scalar [(str proof " is ") []] 22 | :spill [(str (apply util/rc->a1 (:address proof)) " is ") []] 23 | :cell-ref [(str (apply util/rc->a1 (:address proof)) " is ") []]) 24 | dependency-proofs)] 25 | (if (first que) 26 | (let [new-spacer (str spacer " ")] 27 | (str res "\n" new-spacer "where " (string/join (str "\n" new-spacer "and ") (map #(subtree-proof-sentence new-spacer %) que)))) 28 | res))) 29 | 30 | (defn sentence-proof 31 | ([expression sheet] 32 | (let [proof-tree (provenance/ast-proof 33 | (parser/parse (str "=" expression)) 34 | sheet) 35 | [proof-type proof & dependency-proofs] proof-tree 36 | sproof (case proof-type 37 | :scalar (str "it's " proof) 38 | :spill (str "it's " (:scalar proof)) 39 | :cell-ref (str (apply util/rc->a1 (:address proof)) " is " (:scalar proof)))] 40 | (if (coll? (first dependency-proofs)) 41 | (str sproof " because\n" (subtree-proof-sentence "" proof-tree)) 42 | sproof)))) 43 | -------------------------------------------------------------------------------- /src/bean/ui/routes.cljs: -------------------------------------------------------------------------------- 1 | (ns bean.ui.routes 2 | (:require [bidi.bidi :as bidi] 3 | [pushy.core :as pushy] 4 | [re-frame.core :as rf] 5 | [bean.ui.events :as events])) 6 | 7 | (def ^:private app-routes 8 | ["/" {"" :root}]) 9 | 10 | (defn- set-page! [match] 11 | (rf/dispatch-sync [::events/set-route match])) 12 | 13 | (def ^:private history 14 | (pushy/pushy set-page! (partial bidi/match-route app-routes))) 15 | 16 | (defn start [] 17 | (pushy/start! history)) -------------------------------------------------------------------------------- /src/bean/ui/save.cljs: -------------------------------------------------------------------------------- 1 | (ns bean.ui.save 2 | (:require [cljs.reader :as reader])) 3 | 4 | (def db-name "bean-db") 5 | (def store-name "bean-store") 6 | (def store-key "bean-sheet") 7 | (def db-version 1) 8 | 9 | (defn open-db [] 10 | (js/Promise. 11 | (fn [resolve _reject] 12 | (let [request (.open js/indexedDB db-name db-version)] 13 | (set! (.-onupgradeneeded request) 14 | (fn [e] 15 | (let [db (.. e -target -result)] 16 | (.createObjectStore db store-name)))) 17 | (set! (.-onsuccess request) 18 | #(resolve (.. % -target -result))))))) 19 | 20 | (defn write-sheet [sheet] 21 | (-> (open-db) 22 | (.then (fn [db] 23 | (js/Promise. 24 | (fn [resolve _reject] 25 | (let [tx (.transaction db #js [store-name] "readwrite") 26 | store (.objectStore tx store-name) 27 | request (.put store 28 | (str (select-keys sheet [:grid 29 | :depgraph 30 | :frames 31 | :last-frame-number 32 | :grid-dimensions 33 | :code-in-editor 34 | :anthropic-api-key])) 35 | store-key)] 36 | (set! (.-onsuccess request) resolve)))))))) 37 | 38 | (defn read-sheet [] 39 | (-> (open-db) 40 | (.then (fn [db] 41 | (js/Promise. 42 | (fn [resolve _reject] 43 | (let [tx (.transaction db #js [store-name] "readonly") 44 | store (.objectStore tx store-name) 45 | request (.get store store-key)] 46 | (set! (.-onsuccess request) 47 | #(let [result (.. % -target -result)] 48 | (if result 49 | (resolve (reader/read-string result)) 50 | (resolve nil))))))))))) 51 | -------------------------------------------------------------------------------- /src/bean/ui/styles.cljs: -------------------------------------------------------------------------------- 1 | (ns bean.ui.styles) 2 | 3 | (def ^:private light {:sheet-background 0xffffff 4 | :heading-background 0xf2f2f1 5 | :heading-color 0x555555 6 | :grid-line 0x0f0f0f 7 | :resizer-line 0x999999 8 | :heading-border 0xdddddd 9 | :corner-background 0xf2f2f1 10 | :cell-color 0x000000 11 | :cell-error-color 0xb93333 12 | :selection 0x888888 13 | :selection-alpha 0.06 14 | :frame-border 0x3b5aa3 15 | :frame-name 0x3b5aa3 16 | :llm-icon 0x999999}) 17 | 18 | (def colors light) 19 | 20 | (def sizes {:world-h 10000 21 | :world-w 10000 22 | :num-rows 80 23 | :num-cols 16 24 | :cell-h 30 25 | :cell-w 110 26 | :cell-padding 5 27 | :cell-font-size 14 28 | :error-font-size 9 29 | :heading-left-width 35 30 | :heading-font-size 13 31 | :heading-border 1 32 | :resizer-handle 20 33 | :selection-border 1.5 34 | :frame-border 1 35 | :frame-highlight 2 36 | :frame-name-font 12 37 | :frame-name-padding 3}) 38 | 39 | (def cell-background-colors 40 | [nil 0xcccccc 0xb2f2bb 0xa5d8ff 0xffec99]) 41 | -------------------------------------------------------------------------------- /src/bean/ui/subs.cljs: -------------------------------------------------------------------------------- 1 | (ns bean.ui.subs 2 | (:require 3 | [re-frame.core :as re-frame])) 4 | 5 | (re-frame/reg-sub 6 | ::sheet 7 | (fn [db] 8 | (:sheet db))) 9 | 10 | (re-frame/reg-sub 11 | ::frames 12 | (fn [db] 13 | (get-in db [:sheet :frames]))) 14 | 15 | (re-frame/reg-sub 16 | ::ui 17 | (fn [db] 18 | (:ui db))) 19 | 20 | (re-frame/reg-sub 21 | ::editing-cell 22 | (fn [db] 23 | (get-in db [:ui :grid :editing-cell]))) 24 | 25 | (re-frame/reg-sub 26 | ::selection 27 | (fn [db] 28 | (get-in db [:ui :grid :selection]))) 29 | 30 | (re-frame/reg-sub 31 | ::demo-names 32 | (fn [db] 33 | (get-in db [:ui :demo-names]))) 34 | 35 | (re-frame/reg-sub 36 | ::renaming-frame 37 | (fn [db] 38 | (get-in db [:ui :renaming-frame]))) 39 | 40 | (re-frame/reg-sub 41 | ::current-demo-name 42 | (fn [db] 43 | (get-in db [:ui :current-demo-name]))) 44 | 45 | (re-frame/reg-sub 46 | ::route 47 | (fn [db] 48 | (:route db))) 49 | 50 | (re-frame/reg-sub 51 | ::popups 52 | (fn [db] 53 | (get-in db [:ui :popups]))) 54 | 55 | (re-frame/reg-sub 56 | ::anthropic-api-key 57 | (fn [db] 58 | (get-in db [:sheet :anthropic-api-key]))) 59 | 60 | (re-frame/reg-sub 61 | ::asking-llm 62 | (fn [db] 63 | (get-in db [:ui :asking-llm]))) 64 | -------------------------------------------------------------------------------- /src/bean/ui/util.cljs: -------------------------------------------------------------------------------- 1 | (ns bean.ui.util 2 | (:require [clojure.string :as str] 3 | [bean.util :as util])) 4 | 5 | (defn i->a [i] 6 | (apply 7 | str 8 | (loop [a '() 9 | i (inc i)] 10 | (let [m (mod i 26) 11 | n (/ i 26)] 12 | (if (> n 1) 13 | (recur (cons (char (+ 64 m)) a) 14 | n) 15 | (cons (char (+ 64 m)) a)))))) 16 | 17 | (defn rc->a1 [r c] 18 | (str (i->a c) (inc r))) 19 | 20 | (defn cs [& classes] 21 | (->> classes 22 | (remove nil?) 23 | (map name) 24 | (str/join " "))) 25 | 26 | (def map-on-matrix util/map-on-matrix) 27 | (def addresses-matrix util/addresses-matrix) 28 | (def map-on-matrix-addressed util/map-on-matrix-addressed) 29 | (def offset util/offset) 30 | (def distance util/distance) 31 | 32 | (defn color-int->hex [color] 33 | (str "#" (.toString color 16))) 34 | 35 | (defn merged-or-self [[r c] sheet] 36 | (or (get-in sheet [:grid r c :style :merged-with]) [r c])) 37 | 38 | (defn merged-until-or-self [rc sheet] 39 | (let [[r* c*] (merged-or-self rc sheet)] 40 | (or (get-in sheet [:grid r* c* :style :merged-until]) rc))) 41 | 42 | (defn area-inside? [sheet {:keys [start end]}] 43 | (and 44 | (nat-int? (first start)) (nat-int? (second start)) 45 | (< (first end) (get-in sheet [:grid-dimensions :num-rows])) 46 | (< (second end) (get-in sheet [:grid-dimensions :num-cols])))) 47 | -------------------------------------------------------------------------------- /src/bean/ui/views/code.cljs: -------------------------------------------------------------------------------- 1 | (ns bean.ui.views.code 2 | (:require [bean.code :as code] 3 | [re-frame.core :as rf] 4 | [bean.ui.events :as events] 5 | [bean.ui.subs :as subs] 6 | [bean.ui.util :refer [cs]])) 7 | 8 | (defn text-area [] 9 | (let [sheet (rf/subscribe [::subs/sheet])] 10 | [:div {:class :code} 11 | [:div {:class "code-header"} 12 | [:img {:src "img/code-icon.png" 13 | :class :code-icon}] 14 | [:p {:style {:line-height "1.2rem"}} 15 | "Code"] 16 | [:div {:class :code-error} (:code-error @sheet)] 17 | [:button {:class (cs 18 | :small-btn 19 | :dark-mode-btn 20 | (str "code-state-" 21 | (name (or (:code-evaluation-state @sheet) 22 | :evaluated)))) 23 | :on-click #(rf/dispatch [::events/evaluate-code])} 24 | "▶"] 25 | [:button {:class [:small-btn :help-btn] 26 | :on-click #(rf/dispatch [::events/display-help true])} 27 | "?"]] 28 | [:div {:class :code-thick-lines}] 29 | [:div {:class :code-body} 30 | [:div {:class :code-margin}] 31 | [:textarea 32 | ;; TODO: The textarea and the code should keep expanding as more text is added 33 | {:class :code-text 34 | :content-editable "" 35 | :on-change #(rf/dispatch [::events/update-code (.-value (.-target %))]) 36 | :spell-check false 37 | :value (code/get-code @sheet) 38 | :default-value (code/get-code @sheet)}]]])) 39 | -------------------------------------------------------------------------------- /src/bean/ui/views/help.cljs: -------------------------------------------------------------------------------- 1 | (ns bean.ui.views.help 2 | (:require [re-frame.core :as rf] 3 | [bean.ui.events :as events] 4 | [bean.ui.subs :as subs])) 5 | 6 | (defn help [] 7 | (let [ui (rf/subscribe [::subs/ui]) 8 | anthropic-api-key @(rf/subscribe [::subs/anthropic-api-key])] 9 | [:div {:id :help-overlay 10 | :class :help-overlay 11 | :style {:display (if (:help-display @ui) "block" "none")} 12 | :on-click #(rf/dispatch [::events/display-help false])} 13 | [:div {:class :help-container 14 | :on-click #(.stopPropagation %)} 15 | [:div 16 | {:class :help-content} 17 | [:img {:src "help.png" :class :help-light :width "100%"}] 18 | [:img {:src "help-dark.png" :class :help-dark :width "100%"}] 19 | [:p {:class :footer-p} "𖣯 Anthropic API Key"] 20 | [:input {:type "password" 21 | :value anthropic-api-key 22 | :on-change #(rf/dispatch [::events/set-anthropic-api-key (-> % .-target .-value)])}]] 23 | [:div 24 | {:class :help-footer} 25 | [:p {:class :footer-p} 26 | "How to use Bean"] 27 | [:a {:href "https://github.com/nilenso/bean" 28 | :class :footer-github-link} "What is this?"]]]])) 29 | -------------------------------------------------------------------------------- /src/bean/ui/views/popups.cljs: -------------------------------------------------------------------------------- 1 | (ns bean.ui.views.popups 2 | (:require [bean.ui.events :as events] 3 | [bean.ui.subs :as subs] 4 | [re-frame.core :as rf])) 5 | 6 | (defn add-labels [frame-name labels] 7 | (let [dirn-labels (reduce (fn [m [k v]] 8 | (update m v (fnil conj #{}) k)) 9 | {} 10 | labels)] 11 | (doall 12 | (map (fn [[dirn labels]] 13 | (rf/dispatch [::events/add-labels frame-name labels dirn])) 14 | dirn-labels)) 15 | (rf/dispatch [::events/dismiss-popup :add-labels]))) 16 | 17 | (defn add-labels-popup [suggestions] 18 | (let [sheet @(rf/subscribe [::subs/sheet]) 19 | grid (:grid sheet) 20 | {:keys [frame-name labels skip-cells explanation]} suggestions 21 | existing-labels (get-in sheet [:frames frame-name :labels])] 22 | [:div {:class "popup" 23 | :key (random-uuid)} 24 | [:h4 (str "Add labels in $" frame-name)] 25 | (for [[label dirn] labels] 26 | [:div 27 | {:key label} 28 | [:input {:type :checkbox 29 | :id (str label "-label-checkbox") 30 | :defaultChecked (get existing-labels label) 31 | :style {:margin 0 32 | :margin-right "5px"} 33 | :on-change #(if (.-checked (.-target %)) 34 | (rf/dispatch [::events/add-labels frame-name #{label} dirn]) 35 | (rf/dispatch [::events/remove-labels frame-name #{label} dirn]))}] 36 | [:img {:src (str "img/" (name dirn) "-label.png") 37 | :style {:width "20px" 38 | :margin-right "3px" 39 | :margin-bottom "3px" 40 | :vertical-align "middle"}}] 41 | [:label {:style {:margin-right "3px"} 42 | :for (str label "-label-checkbox") 43 | :class :label-suggestion 44 | :on-mouse-over #(rf/dispatch [::events/add-preview-labels frame-name #{label} dirn]) 45 | :on-mouse-out #(rf/dispatch [::events/remove-preview-labels frame-name #{label} dirn])} 46 | (:content (get-in grid label))]]) 47 | 48 | (when-not (empty? skip-cells) 49 | [:span 50 | [:p "Skip cells"] 51 | (for [skip-cell skip-cells] 52 | [:div 53 | {:on-mouse-over #(rf/dispatch [::events/mark-skip-cells frame-name #{skip-cell}]) 54 | :on-mouse-out #(when-not (.-checked (.getElementById js/document (str skip-cell "-skip-cell-checkbox"))) 55 | (rf/dispatch [::events/unmark-skip-cells frame-name #{skip-cell}])) 56 | :key skip-cell} 57 | [:input {:type :checkbox 58 | :id (str skip-cell "-skip-cell-checkbox") 59 | :style {:margin 0 60 | :margin-right "5px"} 61 | :on-change #(if (.-checked (.-target %)) 62 | (rf/dispatch [::events/mark-skip-cells frame-name #{skip-cell}]) 63 | (rf/dispatch [::events/unmark-skip-cells frame-name #{skip-cell}]))}] 64 | [:img {:src (str "img/skip-label.png") 65 | :style {:width "20px" 66 | :margin-right "3px" 67 | :margin-bottom "3px" 68 | :vertical-align "middle"}}] 69 | [:span {:style {:margin-right "3px"}} 70 | (:content (get-in grid skip-cell))]])]) 71 | [:br] 72 | [:details 73 | [:summary [:span {:style {:color "var(--btn-foreground)"}} "Troubleshoot"]] 74 | [:p explanation]] 75 | [:br] 76 | [:button {:class [:controls-btn] 77 | :on-click #(do 78 | (add-labels frame-name labels) 79 | (rf/dispatch [::events/mark-skip-cells frame-name skip-cells]))} 80 | "Accept All"] 81 | [:button {:class [:controls-btn] 82 | :on-click #(rf/dispatch [::events/dismiss-popup :add-labels])} 83 | "Dismiss"]])) 84 | 85 | (defn popups [] 86 | [:div {:class :popups} 87 | (when @(rf/subscribe [::subs/asking-llm]) 88 | [:div {:class [:popup :loader-popup]} 89 | [:div {:class :llm-loader} "𖣯"]]) 90 | (doall 91 | (for [[popup-type popup-data] @(rf/subscribe [::subs/popups])] 92 | (case popup-type 93 | :add-labels (add-labels-popup popup-data) 94 | [])))]) 95 | -------------------------------------------------------------------------------- /src/bean/ui/views/root.cljs: -------------------------------------------------------------------------------- 1 | (ns bean.ui.views.root 2 | (:require [bean.ui.subs :as subs] 3 | [bean.ui.views.help :as help] 4 | [bean.ui.views.popups :as popups] 5 | [bean.ui.views.sheet :as sheet] 6 | [bean.ui.views.sidebar :as sidebar] 7 | [re-frame.core :as rf])) 8 | 9 | (defn root-page [] 10 | (let [ui (rf/subscribe [::subs/ui])] 11 | [:div {:class (when (= (:help-display @ui) "block") 12 | "help-open")} 13 | [help/help] 14 | [:div {:class :container} 15 | [sidebar/sidebar] 16 | [sheet/sheet] 17 | [popups/popups]]])) 18 | 19 | (defn routed [] 20 | (let [route (rf/subscribe [::subs/route])] 21 | (case (:handler @route) 22 | :root [root-page] 23 | [root-page]))) 24 | -------------------------------------------------------------------------------- /src/bean/ui/views/sidebar.cljs: -------------------------------------------------------------------------------- 1 | (ns bean.ui.views.sidebar 2 | (:require [bean.ui.views.code :as code] 3 | [bean.ui.events :as events] 4 | [bean.ui.subs :as subs] 5 | [re-frame.core :as rf] 6 | [bean.area :as area])) 7 | 8 | (defn frames-list [] 9 | (fn [] 10 | (let [frames @(rf/subscribe [::subs/frames]) 11 | selection @(rf/subscribe [::subs/selection]) 12 | renaming-frame @(rf/subscribe [::subs/renaming-frame])] 13 | [:div 14 | [:div {:class :frames-header} 15 | [:img {:src "img/frame-icon.png" 16 | :class :frame-icon}] 17 | [:p {:style {:line-height "1.2rem"}} 18 | "Frames"] 19 | [:button {:class :controls-btn 20 | :style {:margin-left :auto 21 | :margin-right "3px"} 22 | :disabled (area/area-empty? selection) 23 | :on-click #(rf/dispatch [::events/make-frame selection])} 24 | "Make frame"]] 25 | [:div {:class :frames-list-items} 26 | [:div 27 | (doall 28 | (for [[frame-name] frames] 29 | [:div {:key frame-name 30 | :class :frames-list-item} 31 | [:img {:src "img/made-frame-icon.png" 32 | :class :frame-icon}] 33 | (when (not= renaming-frame frame-name) 34 | [:span {:style {:display :inherit}} 35 | [:a {:on-click #(rf/dispatch [::events/renaming-frame frame-name])} 36 | frame-name] 37 | [:a [:img {:src "img/trash-icon.png" 38 | :on-click #(rf/dispatch [::events/remove-frame frame-name]) 39 | :style {:margin-left "10px" 40 | :height "0.8rem"}}]]]) 41 | [:div {:class :tables-list-item} 42 | [:form 43 | {:on-submit #(do (.preventDefault %) 44 | (rf/dispatch [::events/rename-frame 45 | frame-name 46 | (.-value (js/document.getElementById "frame-name-input"))])) 47 | :class [:make-frame-form]} 48 | (when (= renaming-frame frame-name) 49 | [:input {:class :frame-name-input 50 | :id :frame-name-input 51 | :auto-focus true 52 | :on-focus #(.select (.-target %)) 53 | :on-blur #(rf/dispatch [::events/renaming-frame nil]) 54 | :default-value frame-name 55 | :placeholder "Frame name"}])]]]))]]]))) 56 | 57 | 58 | 59 | (defn sidebar [] 60 | [:div {:class :sidebar} 61 | [:div {:class :logo-container} 62 | [:img {:src "img/logo.png" :class :bean-logo}]] 63 | [frames-list] 64 | [code/text-area]]) 65 | -------------------------------------------------------------------------------- /src/bean/util.cljs: -------------------------------------------------------------------------------- 1 | (ns bean.util 2 | (:require [cljs.math :refer [pow]])) 3 | 4 | (def ^:private num-alphabets 26) 5 | 6 | (defn get-cell [grid address] 7 | (if-let [contents (get-in grid address)] 8 | contents 9 | {:error (str "Invalid address " address)})) 10 | 11 | (defn is-expression? [[node-type & _]] 12 | (= node-type :Expression)) 13 | 14 | (defn a1->rc [a n] 15 | (let [indexed-a (map vector (reverse a) (range)) 16 | c (reduce (fn [total [alphabet i]] 17 | (+ total 18 | (* (pow num-alphabets i) 19 | (- (.charCodeAt alphabet 0) 20 | (dec (.charCodeAt "A" 0)))))) 21 | 0 22 | indexed-a)] 23 | [(dec n) (dec c)])) 24 | 25 | (defn map-on-matrix [f matrix] 26 | (mapv #(mapv (fn [element] (f element)) %) matrix)) 27 | 28 | ;; TODO: Is there a better way to return vectors instead of lists 29 | ;; for O(1) lookups later. 30 | (defn map-on-matrix-addressed [f matrix] 31 | (vec (map-indexed (fn [row-idx row] 32 | (vec (map-indexed 33 | (fn [col-idx element] 34 | (f [row-idx col-idx] element)) 35 | row))) 36 | matrix))) 37 | 38 | (defn reduce-on-sheet-addressed [f {:keys [grid] :as sheet}] 39 | (reduce (fn [sheet [addr cell]] 40 | (f sheet addr cell)) 41 | sheet 42 | (mapcat identity (map-on-matrix-addressed vector grid)))) 43 | 44 | (defn matrix-bounds [start-ref end-ref] 45 | (let [[_ start-a start-n] start-ref 46 | [_ end-a end-n] end-ref 47 | start-address (a1->rc start-a (js/parseInt start-n)) 48 | end-address (a1->rc end-a (js/parseInt end-n))] 49 | [start-address end-address])) 50 | 51 | (defn addresses-matrix 52 | [[start-r start-c] [end-r end-c]] 53 | (for [r (range start-r (inc end-r))] 54 | (for [c (range start-c (inc end-c))] 55 | [r c]))) 56 | 57 | (defn random-color-hex 58 | ([] 59 | (random-color-hex (rand-int 1000000))) 60 | ([seed] 61 | (let [hash (hash seed) 62 | r (mod (bit-shift-right hash 16) 256) 63 | g (mod (bit-shift-right hash 8) 256) 64 | b (mod hash 256)] 65 | (+ (bit-shift-left r 16) 66 | (bit-shift-left g 8) 67 | b)))) 68 | 69 | (defn merged-or-self [[r c] sheet] 70 | (or (get-in sheet [:grid r c :style :merged-with]) [r c])) 71 | 72 | (defn offset [[start-r start-c] [offset-rows offset-cols]] 73 | [(+ start-r offset-rows) (+ start-c offset-cols)]) 74 | 75 | (defn distance [[r1 c1] [r2 c2]] 76 | [(- r2 r1) (- c2 c1)]) 77 | -------------------------------------------------------------------------------- /src/bean/value.cljs: -------------------------------------------------------------------------------- 1 | (ns bean.value 2 | (:require [bean.parser.parser :as parser])) 3 | 4 | (defn from-statement 5 | [content ast] 6 | {:content content 7 | :ast ast}) 8 | 9 | (defn from-cell [content] 10 | {:content content 11 | :ast (parser/parse content)}) 12 | 13 | -------------------------------------------------------------------------------- /test/bean/code_test.cljs: -------------------------------------------------------------------------------- 1 | (ns bean.code-test 2 | (:require [clojure.test :refer [deftest testing is]] 3 | [bean.code :as code] 4 | [bean.grid :as grid])) 5 | 6 | (deftest code-errors-test 7 | (testing "When the code has a parse error, it is escalated on reevaluation" 8 | (is (not-empty (-> (grid/new-sheet [[""]] "") 9 | (code/set-code "foo: 99fail") 10 | code/reevaluate 11 | :code-error)))) 12 | 13 | (testing "When the code has an error in a statement, it is escalated on reevaluation" 14 | (is (= (-> (grid/new-sheet [[""]] "") 15 | (code/set-code "foo: B3") 16 | code/reevaluate 17 | :code-error) 18 | "name: foo. Invalid address [2 1]"))) 19 | 20 | (testing "When the code has an error in a statement, it is escalated on initial 21 | evaluation after the grid is evaluated" 22 | (is (= (-> (grid/new-sheet [["1"]] "foo: A1+\"bar\"") 23 | grid/eval-sheet 24 | :code-error) 25 | "name: foo. + only works for Integers"))) 26 | 27 | (testing "When the code has an error in a statement, it is escalated on initial 28 | evaluation after the grid is evaluated but is cleared in a subsequent 29 | evaluation where the error is resolved" 30 | (is (nil? (-> (grid/new-sheet [["1"]] "foo: A1+\"bar\"") 31 | grid/eval-sheet 32 | (code/set-code "foo: A1+20") 33 | code/reevaluate 34 | :code-error))))) 35 | -------------------------------------------------------------------------------- /test/bean/core_test.cljs: -------------------------------------------------------------------------------- 1 | (ns bean.core-test 2 | (:require [clojure.test :refer [run-all-tests]])) 3 | 4 | (comment (run-all-tests)) 5 | -------------------------------------------------------------------------------- /test/bean/deps_test.cljs: -------------------------------------------------------------------------------- 1 | (ns bean.deps-test 2 | (:require [bean.grid :refer [new-sheet eval-sheet]] 3 | [bean.deps :refer [make-depgraph update-depgraph]] 4 | [clojure.test :refer [deftest testing is]])) 5 | 6 | (deftest depgraph-update-test 7 | (testing "Adds an edge to the depgraph to a node that doesn't already exist" 8 | (is 9 | (= {[:cell [0 0]] #{[:cell [0 1]]}} 10 | (let [grid (:grid (eval-sheet (new-sheet [["1" "2"]] "")))] 11 | (update-depgraph (make-depgraph grid) 12 | [:cell [0 1]] 13 | (get-in grid [0 1]) 14 | {:content "=A1" 15 | :ast [:CellContents [:Expression [:CellRef "A" "1"]]] 16 | :scalar 1 17 | :representation "1"}))))) 18 | 19 | (testing "Adds an edge to the depgraph to a node that already has a dependency" 20 | (is 21 | (= {[:cell [0 0]] #{[:cell [0 1]] [:cell [0 2]]}} 22 | (let [grid (:grid (eval-sheet (new-sheet [["1" "2" "=A1"]] "")))] 23 | (update-depgraph (make-depgraph grid) 24 | [:cell [0 1]] 25 | (get-in grid [0 1]) 26 | {:content "=A1" 27 | :ast [:CellContents [:Expression [:CellRef "A" "1"]]] 28 | :scalar 1 29 | :representation "1"}))))) 30 | 31 | (testing "Removes a node from the depgraph if it has no edges after update" 32 | (is 33 | (= {} 34 | (let [grid (:grid (eval-sheet (new-sheet [["1" "=A1"]] "")))] 35 | (update-depgraph (make-depgraph grid) 36 | [:cell [0 1]] 37 | (get-in grid [0 1]) 38 | {:content "2" 39 | :ast [:CellContents [:Number "2"]] 40 | :scalar 2 41 | :representation "2"})))))) 42 | -------------------------------------------------------------------------------- /test/bean/frames_test.cljs: -------------------------------------------------------------------------------- 1 | (ns bean.frames-test 2 | (:require [bean.grid :as grid] 3 | [bean.frames :as frames] 4 | [clojure.test :refer [deftest testing is]])) 5 | 6 | (defn- new-sheet [] 7 | (grid/eval-sheet (grid/new-sheet (repeat 5 (repeat 5 "")) ""))) 8 | 9 | (deftest make-frame-test 10 | (testing "Creates a frame" 11 | (let [sheet (frames/make-frame (new-sheet) "A frame" {:start [0 0] :end [2 2]})] 12 | (is (= (frames/get-frame sheet "A frame") {:start [0 0] :end [2 2] :labels {} :skip-cells #{}}))))) 13 | 14 | (deftest cell-frame-test 15 | (testing "Gets a cell's frame name" 16 | (let [sheet (frames/make-frame (new-sheet) "A frame" {:start [0 0] :end [2 2]})] 17 | (is (= (frames/cell-frame [1 1] sheet) "A frame")) 18 | (is (= (frames/cell-frame [3 3] sheet) nil))))) 19 | 20 | (deftest add-label-test 21 | (testing "Adds labels to a frame" 22 | (let [frame-name "A frame" 23 | sheet (-> (new-sheet) 24 | (frames/make-frame frame-name {:start [0 0] :end [2 2]}) 25 | (frames/add-label frame-name [1 1] :top) 26 | (frames/add-label frame-name [1 2] :left 0x000000))] 27 | (is (= (get-in sheet [:frames frame-name :labels]) {[1 1] {:dirn :top :color nil} 28 | [1 2] {:dirn :left :color 0x000000}}))))) 29 | 30 | (deftest blocking-label 31 | (testing "Top labels of the same direction block labels that exist above" 32 | (let [frame-name "A frame" 33 | sheet (-> (new-sheet) 34 | (frames/make-frame frame-name {:start [0 0] :end [3 3]}) 35 | (frames/add-label frame-name [0 0] :top) 36 | (frames/add-label frame-name [2 0] :top))] 37 | (frames/blocking-label sheet frame-name [0 0]))) 38 | 39 | (testing "Top labels of the same direction and span block labels that exist above" 40 | (let [frame-name "A frame" 41 | sheet (-> (new-sheet) 42 | (frames/make-frame frame-name {:start [0 0] :end [3 3]}) 43 | (frames/add-label frame-name [0 0] :top) 44 | (grid/merge-cells {:start [0 0] :end [0 1]}) 45 | (frames/add-label frame-name [2 0] :top) 46 | (grid/merge-cells {:start [2 0] :end [2 1]}))] 47 | (frames/blocking-label sheet frame-name [0 0])))) 48 | 49 | (deftest label->cells-test 50 | (testing "Gets cells under a simple top label" 51 | (let [frame-name "A frame" 52 | sheet (-> (new-sheet) 53 | (frames/make-frame frame-name {:start [0 0] :end [2 2]}) 54 | (frames/add-label frame-name [1 1] :top))] 55 | (is (= (frames/label->cells sheet frame-name [1 1]) #{[2 1]})))) 56 | 57 | (testing "Gets cells under a simple left label" 58 | (let [frame-name "A frame" 59 | sheet (-> (new-sheet) 60 | (frames/make-frame frame-name {:start [0 0] :end [2 2]}) 61 | (frames/add-label frame-name [1 1] :left))] 62 | (is (= (frames/label->cells sheet frame-name [1 1]) #{[1 2]})))) 63 | 64 | (testing "Gets cells under a merged top label" 65 | (let [frame-name "A frame" 66 | sheet (-> (new-sheet) 67 | (frames/make-frame frame-name {:start [0 0] :end [2 2]}) 68 | (frames/add-label frame-name [1 1] :top) 69 | (grid/merge-cells {:start [1 1] :end [1 2]}))] 70 | (is (= (frames/label->cells sheet frame-name [1 1]) #{[2 1] [2 2]})))) 71 | 72 | (testing "Doesn't include other labels in the result" 73 | (let [frame-name "A frame" 74 | sheet (-> (new-sheet) 75 | (frames/make-frame frame-name {:start [0 0] :end [2 2]}) 76 | (frames/add-label frame-name [0 0] :top) 77 | (grid/merge-cells {:start [0 0] :end [0 1]}) 78 | (frames/add-label frame-name [1 0] :top))] 79 | (is (nil? (get [1 0] (frames/label->cells sheet frame-name [0 0])))))) 80 | 81 | (testing "Includes skip cells from the result" 82 | (let [frame-name "A frame" 83 | sheet (-> (new-sheet) 84 | (frames/make-frame frame-name {:start [0 0] :end [2 2]}) 85 | (frames/add-label frame-name [1 1] :top) 86 | (frames/mark-skipped frame-name [[2 1]]))] 87 | (is (some? (get-in sheet [:frames frame-name :skip-cells [2 1]]))) 88 | (is (some? (get (frames/label->cells sheet frame-name [1 1]) [2 1])))))) 89 | 90 | (deftest skipped-cells-test 91 | (testing "Returns skipped cells and cells under a skip label" 92 | (let [frame-name "A frame" 93 | sheet (-> (new-sheet) 94 | (frames/make-frame frame-name {:start [0 0] :end [2 2]}) 95 | (frames/add-label frame-name [1 1] :top) 96 | (frames/mark-skipped frame-name [[1 1]]))] 97 | (is (some? (get (frames/skipped-cells sheet frame-name) [2 1])))))) 98 | -------------------------------------------------------------------------------- /test/bean/grid_test.cljs: -------------------------------------------------------------------------------- 1 | (ns bean.grid-test 2 | (:require [bean.grid :refer [parse-grid 3 | eval-sheet 4 | eval-code 5 | eval-cell 6 | new-sheet] :as grid] 7 | [bean.deps :refer [make-depgraph]] 8 | [bean.util :as util] 9 | [clojure.test :refer [deftest testing is]] 10 | [bean.area :as area] 11 | [bean.frames :as frames])) 12 | 13 | (deftest evaluator-test 14 | (testing "Basic evaluation" 15 | (let [grid [["1" "" ""] 16 | ["2" "" ""] 17 | ["=A1+A2" "" ""] 18 | ["=A3+1" "" ""] 19 | ["=A1+A2+A3+A4+10" "" ""]] 20 | evaluated-sheet (eval-sheet (new-sheet grid ""))] 21 | (is (= (util/map-on-matrix 22 | #(select-keys % [:scalar :content :error :representation :style]) 23 | (:grid evaluated-sheet)) 24 | [[{:content "1" :scalar 1 :representation "1"} 25 | {:content "" :scalar "" :representation ""} 26 | {:content "" :scalar "" :representation ""}] 27 | [{:content "2" :scalar 2 :representation "2"} 28 | {:content "" :scalar "" :representation ""} 29 | {:content "" :scalar "" :representation ""}] 30 | [{:content "=A1+A2" :scalar 3 :representation "3"} 31 | {:content "" :scalar "" :representation ""} 32 | {:content "" :scalar "" :representation ""}] 33 | [{:content "=A3+1" :scalar 4 :representation "4"} 34 | {:content "" :scalar "" :representation ""} 35 | {:content "" :scalar "" :representation ""}] 36 | [{:content "=A1+A2+A3+A4+10" :scalar 20 :representation "20"} 37 | {:content "" :scalar "" :representation ""} 38 | {:content "" :scalar "" :representation ""}]])) 39 | (is (= (:depgraph evaluated-sheet) 40 | {[:cell [0 0]] #{[:cell [2 0]] [:cell [4 0]]} 41 | [:cell [1 0]] #{[:cell [2 0]] [:cell [4 0]]} 42 | [:cell [2 0]] #{[:cell [3 0]] [:cell [4 0]]} 43 | [:cell [3 0]] #{[:cell [4 0]]}})))) 44 | 45 | (testing "Returns errors" 46 | (let [grid [["=1" "" ""] 47 | ["ABC" "=A1000" ""] 48 | ["=A1+A2" "=B2" ""] 49 | ["=A3+1" "" ""]]] 50 | (is (= (util/map-on-matrix 51 | #(select-keys % [:scalar :content :error :representation]) 52 | (:grid (eval-sheet (new-sheet grid "")))) 53 | [[{:content "=1" :scalar 1 :representation "1"} 54 | {:content "" :scalar "" :representation ""} 55 | {:content "" :scalar "" :representation ""}] 56 | [{:content "ABC" :scalar "ABC" :representation "ABC"} 57 | {:content "=A1000" :scalar nil :error "Invalid address [999 0]" :representation "Invalid address [999 0]"} 58 | {:content "" :scalar "" :representation ""}] 59 | [{:content "=A1+A2" :scalar nil :error "+ only works for Integers" :representation "+ only works for Integers"} 60 | {:content "=B2" :scalar nil :error "Invalid address [999 0]" :representation "Invalid address [999 0]"} 61 | {:content "" :scalar "" :representation ""}] 62 | [{:content "=A3+1" :scalar nil :error "+ only works for Integers" :representation "+ only works for Integers"} 63 | {:content "" :scalar "" :representation ""} 64 | {:content "" :scalar "" :representation ""}]])))) 65 | 66 | (testing "Errors are not operated upon further" 67 | (let [grid [["=A1000+1" "=A1+100" ""]]] 68 | (is (= (util/map-on-matrix 69 | #(select-keys % [:error]) 70 | (:grid (eval-sheet (new-sheet grid "")))) 71 | [[{:error "Invalid address [999 0]"} 72 | {:error "Invalid address [999 0]"} 73 | {}]])))) 74 | 75 | (testing "If a cell has an error, all dependent cells become errors" 76 | (let [grid [["=A1000" "=A1" "=B1"]]] 77 | (is (= (util/map-on-matrix 78 | #(select-keys % [:error]) 79 | (:grid (eval-sheet (new-sheet grid "")))) 80 | [[{:error "Invalid address [999 0]"} 81 | {:error "Invalid address [999 0]"} 82 | {:error "Invalid address [999 0]"}]])))) 83 | 84 | (testing "Matrix evaluation" 85 | (let [grid [["1" ""] 86 | ["2" "=A1:A2"] 87 | ["" ""]] 88 | evaluated-sheet (eval-sheet (new-sheet grid "")) 89 | matrix (get-in evaluated-sheet [:grid 1 1 :matrix])] 90 | (is (= matrix [[{:content "1" 91 | :ast [:CellContents [:Number "1"]] 92 | :scalar 1 93 | :representation "1"}] 94 | [{:content "2" 95 | :ast [:CellContents [:Number "2"]] 96 | :scalar 2 97 | :representation "2"}]])) 98 | (is (= (get-in evaluated-sheet [:grid 1 1 :scalar]) 1)) 99 | (is (= (get-in evaluated-sheet [:grid 1 1 :spilled-into]) #{[1 1] [2 1]})) 100 | (is (= (get-in evaluated-sheet [:grid 2 1 :scalar]) 2)))) 101 | 102 | (testing "Matrix spill errors if a cell has some content" 103 | (let [grid [["1" ""] 104 | ["2" "=A1:A2"] 105 | ["" "A string"]] 106 | evaluated-sheet (eval-sheet (new-sheet grid ""))] 107 | (is (get-in evaluated-sheet [:grid 1 1 :matrix])) 108 | (is (= (get-in evaluated-sheet [:grid 1 1 :error]) "Spill error")) 109 | (is (= (get-in evaluated-sheet [:grid 2 1 :scalar]) "A string")))) 110 | 111 | (testing "Matrix spill errors if there's a conflict" 112 | (let [grid [["1" "3"] 113 | ["2" "=A1:A2"] 114 | ["=A1:B1" ""]] 115 | evaluated-sheet (eval-sheet (new-sheet grid ""))] 116 | (is (= (get-in evaluated-sheet [:grid 1 1 :scalar]) 1)) 117 | (is (= (get-in evaluated-sheet [:grid 2 1 :scalar]) 2)) 118 | (is (= (get-in evaluated-sheet [:grid 2 0 :error]) "Spill error")) 119 | (is (= (get-in evaluated-sheet [:grid 2 0 :representation]) "Spill error")) 120 | (is (= (get-in evaluated-sheet [:grid 2 0 :scalar]) nil)) 121 | (is (= (util/map-on-matrix :representation (:grid evaluated-sheet)) 122 | [["1" "3"] 123 | ["2" "1"] 124 | ["Spill error" "2"]])))) 125 | 126 | (testing "Cells depending on spillages are evaluated" 127 | (let [grid (:grid (eval-sheet (new-sheet [["1" "" "=A1:A3"] 128 | ["2" "" ""] 129 | ["3" "=C2" ""] 130 | ["=B3" "=A3:A5" ""] 131 | ["" "" ""] 132 | ["" "" ""]] 133 | "")))] 134 | (is (= (get-in grid [2 1 :scalar]) 2)) 135 | (is (= (get-in grid [3 0 :scalar]) 2)) 136 | (is (= (get-in grid [3 1 :scalar]) 3)) 137 | (is (= (get-in grid [4 1 :scalar]) 2)))) 138 | 139 | (testing "Spill errors are re-evaluated when conflicting value is cleared" 140 | (let [sheet (eval-sheet (new-sheet [["1" "10" "20" ""] 141 | ["2" "" "" "=A1:A3"] 142 | ["3" "" "=A1:C1" "Cross"]] 143 | "")) 144 | evaluated-grid (:grid (eval-cell [2 3] sheet ""))] 145 | (is (= (get-in evaluated-grid [1 3 :scalar]) 1)) 146 | (is (= (get-in evaluated-grid [2 3 :scalar]) 2)) 147 | (is (= (get-in evaluated-grid [2 2 :error]) "Spill error")))) 148 | 149 | (testing "Function invocation" 150 | (is (= (util/map-on-matrix 151 | #(select-keys % [:scalar :content :error :representation]) 152 | (:grid (eval-sheet (new-sheet [["1" "=concat(\"hello \", A1, A2)" ""] 153 | ["2" "" ""] 154 | ["=A1+A2" "" ""] 155 | ["=A3+1" "" ""] 156 | ["=A1+A2+A3+A4+10" "" ""]] 157 | "")))) 158 | [[{:content "1" :scalar 1 :representation "1"} 159 | {:content "=concat(\"hello \", A1, A2)" :scalar "hello 12" :representation "hello 12"} 160 | {:content "" :scalar "" :representation ""}] 161 | [{:content "2" :scalar 2 :representation "2"} 162 | {:content "" :scalar "" :representation ""} 163 | {:content "" :scalar "" :representation ""}] 164 | [{:content "=A1+A2" :scalar 3 :representation "3"} 165 | {:content "" :scalar "" :representation ""} 166 | {:content "" :scalar "" :representation ""}] 167 | [{:content "=A3+1" :scalar 4 :representation "4"} 168 | {:content "" :scalar "" :representation ""} 169 | {:content "" :scalar "" :representation ""}] 170 | [{:content "=A1+A2+A3+A4+10" :scalar 20 :representation "20"} 171 | {:content "" :scalar "" :representation ""} 172 | {:content "" :scalar "" :representation ""}]]))) 173 | 174 | (testing "Inlined function invocation" 175 | (is (= (util/map-on-matrix 176 | #(select-keys % [:scalar :content :error :representation]) 177 | (:grid (eval-sheet (new-sheet [["1" "={x+y+z}(9, A1, A2)"] 178 | ["2" ""]] 179 | "")))) 180 | [[{:content "1" :scalar 1 :representation "1"} 181 | {:content "={x+y+z}(9, A1, A2)" :scalar 12 :representation "12"}] 182 | [{:content "2" :scalar 2 :representation "2"} 183 | {:content "" :scalar "" :representation ""}]])))) 184 | 185 | (deftest incremental-evaluate-grid 186 | (testing "Basic incremental evaluation given a pre-evaluated grid and a depgraph" 187 | (let [sheet (eval-sheet (new-sheet [["10" "=A1" "=A1+B1" "100" "=C1" "=A1"]] 188 | "")) 189 | {evaluated-grid :grid depgraph :depgraph} (eval-cell [0 1] sheet "=A1+D1")] 190 | (is (= 10 (:scalar (util/get-cell evaluated-grid [0 0])))) 191 | (is (= 110 (:scalar (util/get-cell evaluated-grid [0 1])))) 192 | (is (= 120 (:scalar (util/get-cell evaluated-grid [0 2])))) 193 | (is (= 100 (:scalar (util/get-cell evaluated-grid [0 3])))) 194 | (is (= 120 (:scalar (util/get-cell evaluated-grid [0 4])))) 195 | (is (= depgraph 196 | {[:cell [0 0]] #{[:cell [0 1]] [:cell [0 2]] [:cell [0 5]]} 197 | [:cell [0 1]] #{[:cell [0 2]]} 198 | [:cell [0 2]] #{[:cell [0 4]]} 199 | [:cell [0 3]] #{[:cell [0 1]]}})))) 200 | 201 | (testing "Older dependencies are removed in an incremental evaluation" 202 | (let [sheet (eval-sheet (new-sheet [["10" "=A1" "=A1+B1" "100"]] 203 | "")) 204 | {depgraph :depgraph} (eval-cell [0 1] sheet "=D1")] 205 | (is (= depgraph 206 | {[:cell [0 0]] #{[:cell [0 2]]} 207 | [:cell [0 1]] #{[:cell [0 2]]} 208 | [:cell [0 3]] #{[:cell [0 1]]}})))) 209 | 210 | (testing "Cells containing matrix references are spilled" 211 | (let [sheet (eval-sheet (new-sheet [["10" ""] 212 | ["20" ""] 213 | ["30" ""]] 214 | "")) 215 | {evaluated-grid :grid depgraph :depgraph} (eval-cell [0 1] sheet "=A1:A3")] 216 | (is (= (util/map-on-matrix :representation evaluated-grid) 217 | [["10" "10"] 218 | ["20" "20"] 219 | ["30" "30"]])) 220 | (is (get-in evaluated-grid [0 1 :matrix])) 221 | (is (= depgraph {[:cell [0 0]] #{[:cell [0 1]]} 222 | [:cell [1 0]] #{[:cell [0 1]]} 223 | [:cell [2 0]] #{[:cell [0 1]]}})))) 224 | 225 | (testing "If the address is referred somewhere in a matrix reference, the matrix reference is re-evaluated and spilled" 226 | (let [sheet (eval-sheet (new-sheet [["10" "=A1:A3"] 227 | ["" ""] 228 | ["" ""] 229 | ["=B2" ""]] 230 | "")) 231 | {evaluated-grid :grid depgraph :depgraph} (eval-cell [1 0] sheet "20")] 232 | (is (= (util/map-on-matrix :representation evaluated-grid) 233 | [["10" "10"] 234 | ["20" "20"] 235 | ["" ""] 236 | ["20" ""]])))) 237 | 238 | (testing "If content is added to a spilled cell, the origin results in a spill error" 239 | (let [sheet (eval-sheet (new-sheet [["10" "=A1:A3"] 240 | ["20" ""] 241 | ["" ""]] 242 | "")) 243 | {evaluated-grid :grid} (eval-cell [1 1] sheet "A string")] 244 | (is (= (util/map-on-matrix :representation evaluated-grid) 245 | [["10" "Spill error"] 246 | ["20" "A string"] 247 | ["" ""]])))) 248 | 249 | (testing "Unorderly references" 250 | (let [sheet (eval-sheet (new-sheet [["=B1:B4" "8" "=D3" "19"] 251 | ["" "1" "2" "4"] 252 | ["" "=C1:D2" "" "=C2+D2"] 253 | ["" "" "" ""]] 254 | "")) 255 | {evaluated-grid :grid} (eval-cell [1 2] sheet "202")] 256 | (is (= (util/map-on-matrix :representation (:grid sheet)) 257 | [["8" "8" "6" "19"] 258 | ["1" "1" "2" "4"] 259 | ["6" "6" "19" "6"] 260 | ["2" "2" "4" ""]])) 261 | (is (= (util/map-on-matrix :representation evaluated-grid) 262 | [["8" "8" "206" "19"] 263 | ["1" "1" "202" "4"] 264 | ["206" "206" "19" "206"] 265 | ["202" "202" "4" ""]])))) 266 | 267 | (testing "Styles are preserved after evaluation" 268 | (let [sheet (eval-sheet (new-sheet [["1" "=A1"]] "")) 269 | style {:background 0x000000} 270 | styled-sheet (assoc-in sheet [:grid 0 0 :style] style) 271 | {evaluated-grid :grid} (eval-cell [0 1] styled-sheet "=A1+1")] 272 | (is (= (get-in evaluated-grid [0 0 :style]) style))))) 273 | 274 | (deftest depgraph-test 275 | (testing "Returns a reverse dependency graph for an evaluated grid" 276 | (is (= (make-depgraph (parse-grid [["10" "=A1" "=A1+B1" "=C1"]])) 277 | {[:cell [0 0]] #{[:cell [0 2]] [:cell [0 1]]} 278 | [:cell [0 1]] #{[:cell [0 2]]} 279 | [:cell [0 2]] #{[:cell [0 3]]}})))) 280 | 281 | (deftest map-on-matrix-test 282 | (testing "Row order map of f over a 2D matrix" 283 | (let [matrix [[10 20 30] 284 | [40 50 60]]] 285 | (is (= (util/map-on-matrix identity matrix) matrix))))) 286 | 287 | (deftest map-on-matrix-addressed-test 288 | (testing "Row order map of f over a 2D matrix with address also supplied to f" 289 | (let [matrix [[10 20 30] 290 | [40 50 60]]] 291 | (is (= (util/map-on-matrix-addressed 292 | (fn [address item] [address item]) matrix) 293 | [[[[0 0] 10] [[0 1] 20] [[0 2] 30]] 294 | [[[1 0] 40] [[1 1] 50] [[1 2] 60]]]))))) 295 | 296 | (deftest named-reference-evaluation-test 297 | (testing "A named reference is evaluated" 298 | (is (= [["1" "2" "3"]] 299 | (->> (new-sheet [["1" "2" "=addthree(1, 1, 1)"]] "addthree:{x+y+z}") 300 | eval-sheet 301 | :grid 302 | (util/map-on-matrix :representation))))) 303 | 304 | (testing "A named reference's dependents are re-evaluated" 305 | (is (= [["11" "2" "20"]] 306 | (as-> (new-sheet [["1" "2" "=addaone(9)"]] "addaone:{x+A1}") sheet 307 | (eval-sheet sheet) 308 | (eval-cell [0 0] sheet "=11") 309 | (util/map-on-matrix :representation (:grid sheet)))))) 310 | 311 | (testing "A named reference is re-evaluated when its dependency changes" 312 | (is (= 10 313 | (as-> (new-sheet [["1" "2"]] "addaone:4+A1") sheet 314 | (eval-sheet sheet) 315 | (eval-cell [0 0] sheet "6") 316 | (get-in sheet [:bindings "addaone" :scalar]))))) 317 | 318 | (testing "Depgraph is updated when a named reference's dependencies change" 319 | (is (= {[:cell [0 1]] #{[:named "addaone"]}} 320 | (as-> (new-sheet [["1" "2"]] "addaone:4+A1") sheet 321 | (eval-sheet sheet) 322 | (eval-code sheet "addaone:4+B1") 323 | (get-in sheet [:depgraph]))))) 324 | 325 | (testing "Depgraph is updated when a named reference's dependents change" 326 | (is (= {[:named "addaone"] #{[:cell [0 1]]}} 327 | (as-> (new-sheet [["1" "2"]] "addaone:4") sheet 328 | (eval-sheet sheet) 329 | (eval-cell [0 1] sheet "=addaone+20") 330 | (get-in sheet [:depgraph])))))) 331 | 332 | (deftest merge-cells-test 333 | (testing "Merges cells given an area" 334 | (let [sheet (-> (new-sheet (repeat 5 (repeat 5 "")) "") 335 | eval-sheet 336 | (grid/merge-cells {:start [0 0] :end [1 2]}))] 337 | (is (= (get-in sheet [:grid 0 0 :style]) {:merged-with [0 0] 338 | :merged-until [1 2] 339 | :merged-addresses #{[0 0] [0 1] [0 2] [1 0] [1 1] [1 2]}})) 340 | (is (= (area/cell-h sheet [0 0]) 2)) 341 | (is (= (area/cell-w sheet [0 0]) 3)))) 342 | 343 | (testing "Merges existing merged cells only if the full merged cell is in the area" 344 | (let [sheet (-> (new-sheet (repeat 5 (repeat 5 "")) "") 345 | eval-sheet 346 | (grid/merge-cells {:start [0 0] :end [1 1]}) 347 | (grid/merge-cells {:start [1 1] :end [1 2]}))] 348 | (is (= (select-keys (get-in sheet [:grid 0 0 :style]) 349 | [:merged-with :merged-until]) 350 | {:merged-with [0 0] 351 | :merged-until [1 1]})) 352 | (is (= (select-keys (get-in (grid/merge-cells sheet {:start [0 0] :end [1 2]}) 353 | [:grid 0 0 :style]) 354 | [:merged-with :merged-until]) 355 | {:merged-with [0 0] 356 | :merged-until [1 2]})))) 357 | 358 | (testing "Merges labels into a single label" 359 | (let [frame-name "A frame" 360 | sheet (-> (new-sheet (repeat 5 (repeat 5 "")) "") 361 | eval-sheet 362 | (frames/make-frame frame-name {:start [0 0] :end [2 2]}) 363 | (frames/add-label frame-name [1 1] :top) 364 | (grid/merge-cells {:start [0 1] :end [1 1]}))] 365 | (is (= (get-in sheet [:frames frame-name :labels]) {[0 1] {:dirn :top :color nil}}))))) 366 | -------------------------------------------------------------------------------- /test/bean/interpreter_test.cljs: -------------------------------------------------------------------------------- 1 | (ns bean.interpreter-test 2 | (:require [bean.grid :as grid] 3 | [bean.interpreter :as interpreter] 4 | [bean.operators :as operators] 5 | [bean.parser.parser :as parser] 6 | [clojure.test :refer [deftest is testing]])) 7 | 8 | (defn- new-sheet [] 9 | (grid/eval-sheet (grid/new-sheet (repeat 5 (repeat 5 "")) ""))) 10 | 11 | (deftest apply-op-test 12 | (testing "Applies an op to a matrix and a scalar" 13 | (is (= (interpreter/apply-op {:scalar operators/bean-op-+} {:scalar 1} 14 | {:matrix [[{:scalar 1} 15 | {:scalar 2} 16 | {:scalar 1}]]}) 17 | {:matrix 18 | [[{:scalar 2 :representation "2"} 19 | {:scalar 3 :representation "3"} 20 | {:scalar 2 :representation "2"}]]}))) 21 | 22 | (testing "Applies an op to a scalar and a matrix" 23 | (is (= (interpreter/apply-op {:scalar operators/bean-op-+} 24 | {:matrix [[{:scalar 1} 25 | {:scalar 2} 26 | {:scalar 1}]]} 27 | {:scalar 1}) 28 | {:matrix 29 | [[{:scalar 2 :representation "2"} 30 | {:scalar 3 :representation "3"} 31 | {:scalar 2 :representation "2"}]]}))) 32 | 33 | (testing "Applies an op to a scalar and a matrix" 34 | (is (= (interpreter/apply-op {:scalar operators/bean-op-+} 35 | {:matrix [[{:scalar 1} 36 | {:scalar 2} 37 | {:scalar 1}]]} 38 | {:matrix [[{:scalar 1} 39 | {:scalar 2} 40 | {:scalar 1}]]}) 41 | {:matrix 42 | [[{:scalar 2 :representation "2"} 43 | {:scalar 4 :representation "4"} 44 | {:scalar 2 :representation "2"}]]})))) 45 | 46 | (deftest function-chain-test 47 | (testing "Passes expression as the first argument to function at end of chain" 48 | (let [sheet (update-in 49 | (new-sheet) [:bindings] 50 | merge 51 | {"one" {:content "{1}" 52 | :ast [:Expression [:FunctionDefinition [:Expression [:Value [:Number "1"]]]]] 53 | :scalar [:Expression [:Value [:Number "1"]]] 54 | :representation ""} 55 | "inc" {:content "{x+1}" 56 | :ast [:Expression [:FunctionDefinition [:Expression [:Expression [:Name "x"]] [:Operation "+"] [:Expression [:Value [:Number "1"]]]]]] 57 | :scalar [:Expression [:Expression [:Name "x"]] [:Operation "+"] [:Expression [:Value [:Number "1"]]]] 58 | :representation ""} 59 | "add" {:content "{x+y}" 60 | :ast [:Expression [:FunctionDefinition [:Expression [:Expression [:Name "x"]] [:Operation "+"] [:Expression [:Name "y"]]]]] 61 | :scalar [:Expression [:Expression [:Name "x"]] [:Operation "+"] [:Expression [:Name "y"]]] 62 | :representation ""}})] 63 | (is (= (:scalar (interpreter/eval-ast (parser/parse "=one().inc()") sheet)) 2)) 64 | (is (= (:scalar (interpreter/eval-ast (parser/parse "=one().inc().add(20)") sheet)) 22))))) 65 | -------------------------------------------------------------------------------- /test/bean/operators_test.cljs: -------------------------------------------------------------------------------- 1 | (ns bean.operators-test 2 | (:require [bean.operators :refer [bean-op-+ bean-op-* bean-op-minus bean-op-div]] 3 | [clojure.test :refer [deftest testing is]])) 4 | 5 | (deftest bean-op-+-test 6 | (testing "Adds two numbers" 7 | (is (= (bean-op-+ 2 3) 5))) 8 | (testing "Returns an error if an operand is an invalid data type" 9 | (is (= (bean-op-+ "1" 2) {:error "+ only works for Integers" 10 | :representation "+ only works for Integers"})))) 11 | 12 | (deftest bean-op-divide-test 13 | (testing "Divides" 14 | (is (= (bean-op-div 6 3) 2))) 15 | (testing "Returns an error if one operand is a string" 16 | (is (= (bean-op-div 6 "3") {:error "/ only works for Integers" 17 | :representation "/ only works for Integers"}))) 18 | (testing "Returns an error when dividing by zero" 19 | (is (= (bean-op-div 6 0) {:error "cannot divide by zero" 20 | :representation "cannot divide by zero"})))) 21 | 22 | 23 | (deftest bean-op-*-test 24 | (testing "Multiplies two numbers" 25 | (is (= (bean-op-* 2 3) 6))) 26 | (testing "Returns an error if one operand is a string" 27 | (is (= (bean-op-* 1 "2") {:error "* only works for Integers" 28 | :representation "* only works for Integers"})) 29 | (is (= (bean-op-* "1" 2) {:error "* only works for Integers" 30 | :representation "* only works for Integers"})))) 31 | -------------------------------------------------------------------------------- /test/bean/parser/trellis_parser_test.cljs: -------------------------------------------------------------------------------- 1 | (ns bean.parser.trellis-parser-test 2 | (:require [clojure.test :refer [deftest testing is]] 3 | [clojure.string :as str] 4 | [bean.parser.trellis-parser :as trellis-parser])) 5 | 6 | (deftest trellis-parser-test 7 | (testing "Leaf file format parsing" 8 | (is (= (trellis-parser/parse (str/join "\n\n%\n\n" ["foo:10" 9 | "1,2\n3,4" 10 | "A1= 1"])) 11 | [:TrellisFile 12 | [:Program 13 | [:LetStatement 14 | [:Name "foo"] 15 | [:Expression [:Value [:Number "10"]]]]] 16 | [["1" "2"] ["3" "4"]] 17 | [:TestProgram 18 | [:AssertionStatement 19 | [:Expression [:CellRef "A" "1"]] 20 | [:Expression [:Value [:Number "1"]]]]]]))) 21 | (testing "Parsing tests code" 22 | (is (= (trellis-parser/parse-tests "A1=8\n1+1=10") 23 | [:TestProgram 24 | [:AssertionStatement 25 | [:Expression [:CellRef "A" "1"]] 26 | [:Expression [:Value [:Number "8"]]]] 27 | [:AssertionStatement 28 | [:Expression [:Expression [:Value [:Number "1"]]] [:Operation "+"] [:Expression [:Value [:Number "1"]]]] 29 | [:Expression [:Value [:Number "10"]]]]])))) 30 | -------------------------------------------------------------------------------- /test/bean/parser_test.cljs: -------------------------------------------------------------------------------- 1 | (ns bean.parser-test 2 | (:require [bean.parser.parser :refer [parse parse-statement statement-source]] 3 | [clojure.test :refer [deftest testing is]])) 4 | 5 | (deftest parser-test 6 | (testing "Basic Parsing" 7 | (is (= [:CellContents [:Number "4"]] 8 | (parse "4"))) 9 | (is (= [:CellContents [:String "foo"]] 10 | (parse "foo"))) 11 | (is (= [:CellContents [:Expression [:Value [:Number "89"]]]] 12 | (parse "=89"))) 13 | (is (= [:CellContents [:Expression [:Value [:QuotedString "foo"]]]] 14 | (parse "=\"foo\""))) 15 | (is (= [:CellContents [:Expression [:CellRef "A" "8"]]] 16 | (parse "=A8"))) 17 | (is (= [:CellContents [:Expression 18 | [:Expression [:CellRef "A" "8"]] 19 | [:Operation "+"] 20 | [:Expression [:CellRef "B" "9"]]]] 21 | (parse "=A8+B9"))) 22 | (is (= [:CellContents [:Expression 23 | [:FunctionInvocation 24 | [:Name "concat"] 25 | [:Expression [:Value [:QuotedString "hello"]]] 26 | [:Expression [:CellRef "A" "3"]] 27 | [:Expression [:CellRef "A" "4"]]]]] 28 | (parse "=concat(\"hello\", A3, A4)") 29 | (parse "=concat(\"hello\",A3 ,A4)"))))) 30 | 31 | (deftest string-parsing-test 32 | (testing "Empty strings are parsed" 33 | (is (= [:CellContents [:String ""]] 34 | (parse "")))) 35 | 36 | (testing "Unicode strings are parsed" 37 | (is (= [:CellContents [:String "😀🥲🤪🤑"]] 38 | (parse "😀🥲🤪🤑"))))) 39 | 40 | (deftest parse-statement-test 41 | (testing "Statement parsing" 42 | (is (= [:Program] 43 | (parse-statement ""))) 44 | (is (= [:Program 45 | [:LetStatement [:Name "foo"] [:Expression [:Value [:Number "99"]]]] 46 | [:LetStatement [:Name "bar"] [:Expression 47 | [:Expression [:CellRef "A" "1"]] 48 | [:Operation "+"] 49 | [:Expression [:Value [:Number "9"]]]]]] 50 | (parse-statement "foo:99\n\n\nbar :A1+9")))) 51 | (let [src "foo:99\n\n\nbar :A1+9" 52 | evald (parse-statement src)] 53 | 54 | (map #(statement-source src %) 55 | (rest evald)))) 56 | -------------------------------------------------------------------------------- /test/bean/provenance_test.cljs: -------------------------------------------------------------------------------- 1 | (ns bean.provenance-test 2 | (:require [clojure.test :refer [deftest testing is run-all-tests]] 3 | [bean.provenance :as provenance] 4 | [bean.grid :as grid])) 5 | 6 | (deftest provenance-test 7 | (testing "Provenance for simple expressions" 8 | (let [evaled-grid (grid/eval-sheet 9 | (grid/new-sheet [["" "1" "=1+2" "=1+2+3"]] ""))] 10 | (is (= (provenance/cell-proof [0 0] evaled-grid) 11 | [:cell-ref 12 | {:address [0 0] :scalar "" :content ""} 13 | [:scalar "" :self-evident]])) 14 | (is (= (provenance/cell-proof [0 1] evaled-grid) 15 | [:cell-ref 16 | {:address [0 1] :scalar 1 :content "1"} 17 | [:scalar 1 :self-evident]])) 18 | (is (= (provenance/cell-proof [0 2] evaled-grid) 19 | [:cell-ref 20 | {:address [0 2] :scalar 3 :content "=1+2"} 21 | [:scalar 3 22 | [:scalar 1 :self-evident] 23 | [:scalar "+" :self-evident] 24 | [:scalar 2 :self-evident]]])) 25 | (is (= (provenance/cell-proof [0 3] evaled-grid) 26 | [:cell-ref 27 | {:address [0 3] :scalar 6 :content "=1+2+3"} 28 | [:scalar 6 29 | [:scalar 1 :self-evident] 30 | [:scalar "+" :self-evident] 31 | [:scalar 5 32 | [:scalar 2 :self-evident] 33 | [:scalar "+" :self-evident] 34 | [:scalar 3 :self-evident]]]])))) 35 | 36 | (testing "Provenance for cell references" 37 | (let [evaled-grid (grid/eval-sheet 38 | (grid/new-sheet [["1" "=A1" "=B1"]] ""))] 39 | (is (= (provenance/cell-proof [0 1] evaled-grid) 40 | [:cell-ref 41 | {:address [0 1] :content "=A1" :scalar 1} 42 | [:cell-ref 43 | {:address [0 0] :content "1" :scalar 1} 44 | [:scalar 1 :self-evident]]])) 45 | (is (= (provenance/cell-proof [0 2] evaled-grid) 46 | [:cell-ref 47 | {:address [0 2] :content "=B1" :scalar 1} 48 | [:cell-ref 49 | {:address [0 1] :content "=A1" :scalar 1} 50 | [:cell-ref 51 | {:address [0 0] :content "1" :scalar 1} 52 | [:scalar 1 :self-evident]]]])))) 53 | 54 | (testing "Provenance for multiple cell references" 55 | (let [evaled-grid (grid/eval-sheet 56 | (grid/new-sheet [["1" "2" "=A1+B1"] ["=C1" "" ""]] ""))] 57 | (is (= (provenance/cell-proof [0 2] evaled-grid) 58 | [:cell-ref 59 | {:address [0 2] :content "=A1+B1" :scalar 3} 60 | [:scalar 3 61 | [:cell-ref 62 | {:address [0 0] :content "1" :scalar 1} 63 | [:scalar 1 :self-evident]] 64 | [:scalar "+" :self-evident] 65 | [:cell-ref 66 | {:address [0 1] :content "2" :scalar 2} 67 | [:scalar 2 :self-evident]]]])) 68 | (is (= (provenance/cell-proof [1 0] evaled-grid) 69 | [:cell-ref 70 | {:address [1 0] :content "=C1" :scalar 3} 71 | [:cell-ref 72 | {:address [0 2] :content "=A1+B1" :scalar 3} 73 | [:scalar 3 74 | [:cell-ref 75 | {:address [0 0] :content "1" :scalar 1} 76 | [:scalar 1 :self-evident]] 77 | [:scalar "+" :self-evident] 78 | [:cell-ref 79 | {:address [0 1] :content "2" :scalar 2} 80 | [:scalar 2 :self-evident]]]]])))) 81 | 82 | (testing "Provenance of spilled cells" 83 | (let [evaled-grid (grid/eval-sheet 84 | (grid/new-sheet [["1" "=A1:A2" ""] ["2" "" ""]] ""))] 85 | (is (= (provenance/cell-proof [0 1] evaled-grid) 86 | [:spill 87 | {:spilled-from [0 1] 88 | :content "=A1:A2" 89 | :address [0 1] 90 | :scalar 1 91 | :relative-address [0 0]} 92 | [:cell-ref 93 | {:address [0 0] :content "1" :scalar 1} 94 | [:scalar 1 :self-evident]]])) 95 | (is (= (provenance/cell-proof [1 1] evaled-grid) 96 | [:spill 97 | {:spilled-from [0 1] 98 | :content "=A1:A2" 99 | :address [1 1] 100 | :scalar 2 101 | :relative-address [1 0]} 102 | [:cell-ref 103 | {:address [1 0] :content "2" :scalar 2} 104 | [:scalar 2 :self-evident]]])))) 105 | 106 | (testing "Provenance for matrix opertions" 107 | (let [evaled-grid (grid/eval-sheet 108 | (grid/new-sheet [["1" "=A1:A2+C1:C2" "3"] 109 | ["2" "" "=A1"]] 110 | ""))] 111 | (is (= (provenance/cell-proof [1 1] evaled-grid) 112 | [:spill 113 | {:spilled-from [0 1] 114 | :content "=A1:A2+C1:C2" 115 | :address [1 1] 116 | :scalar 3 117 | :relative-address [1 0]} 118 | [:scalar 3 119 | [:cell-ref 120 | {:address [1 0] :content "2" :scalar 2} 121 | [:scalar 2 :self-evident]] 122 | [:scalar "+" :self-evident] 123 | [:cell-ref 124 | {:address [1 2] :content "=A1" :scalar 1} 125 | [:cell-ref 126 | {:address [0 0] :content "1" :scalar 1} 127 | [:scalar 1 :self-evident]]]]])))) 128 | 129 | (testing "Provenance for matrix-scalar operations" 130 | (let [evaled-grid (grid/eval-sheet (grid/new-sheet [["1" "=A1:A2+1"] 131 | ["2" ""]] 132 | ""))] 133 | (is (= (provenance/cell-proof [1 1] evaled-grid) 134 | [:spill 135 | {:spilled-from [0 1] 136 | :content "=A1:A2+1" 137 | :address [1 1] 138 | :scalar 3 139 | :relative-address [1 0]} 140 | [:scalar 3 141 | [:cell-ref 142 | {:address [1 0] :content "2" :scalar 2} 143 | [:scalar 2 :self-evident]] 144 | [:scalar "+" :self-evident] 145 | [:scalar 1 :self-evident]]]))) 146 | 147 | (let [evaled-grid (grid/eval-sheet 148 | (grid/new-sheet [["1" "=A1:A2+C1:C2+D1:D2+1" "3" "0"] 149 | ["2" "" "4" "0"]] 150 | ""))] 151 | (is (= (provenance/cell-proof [1 1] evaled-grid) 152 | [:spill 153 | {:spilled-from [0 1] 154 | :content "=A1:A2+C1:C2+D1:D2+1" 155 | :address [1 1] 156 | :scalar 7 157 | :relative-address [1 0]} 158 | [:scalar 7 159 | [:cell-ref 160 | {:address [1 0] :content "2" :scalar 2} 161 | [:scalar 2 :self-evident]] 162 | [:scalar "+" :self-evident] 163 | [:scalar 5 164 | [:cell-ref 165 | {:address [1 2] :content "4" :scalar 4} 166 | [:scalar 4 :self-evident]] 167 | [:scalar "+" :self-evident] 168 | [:scalar 1 169 | [:cell-ref 170 | {:address [1 3] :content "0" :scalar 0} 171 | [:scalar 0 :self-evident]] 172 | [:scalar "+" :self-evident] 173 | [:scalar 1 :self-evident]]]]]))))) 174 | -------------------------------------------------------------------------------- /test/bean/trellis_test.cljs: -------------------------------------------------------------------------------- 1 | (ns bean.trellis-test 2 | (:require [cljs-node-io.fs :as fs] 3 | [bean.trellis :as trellis] 4 | [clojure.test :refer [deftest testing]])) 5 | 6 | (deftest trellis-test 7 | (testing "Run all the trellis tests" 8 | (fs/crawl "./test/trellis" (fn [path] 9 | (when (and (fs/file? path) 10 | (re-matches #".*\.leaf$" path)) 11 | (print "Testing" (str (-> path fs/dirname fs/basename) "/" (fs/basename path))) 12 | (trellis/execute path)))))) -------------------------------------------------------------------------------- /test/bean/value_test.cljs: -------------------------------------------------------------------------------- 1 | (ns bean.value-test 2 | (:require [clojure.test :refer [deftest is testing]] 3 | [bean.value :as value])) 4 | 5 | (deftest new-test 6 | (testing "Returns a new value for a statement with given content & ast" 7 | (is (= {:ast [:Expression 8 | [:Expression [:CellRef "A" "8"]] 9 | [:Operation "+"] 10 | [:Expression [:CellRef "B" "9"]]] 11 | :content "A8+B9"} 12 | (value/from-statement "A8+B9" [:Expression 13 | [:Expression [:CellRef "A" "8"]] 14 | [:Operation "+"] 15 | [:Expression [:CellRef "B" "9"]]])))) 16 | (testing "Returns a value that parses given cell contents & uses that ast" 17 | (is (= {:ast [:CellContents [:Expression 18 | [:Expression [:CellRef "A" "8"]] 19 | [:Operation "+"] 20 | [:Expression [:CellRef "B" "9"]]]] 21 | :content "=A8+B9"} 22 | (value/from-cell "=A8+B9"))))) -------------------------------------------------------------------------------- /test/trellis/addition_test.leaf: -------------------------------------------------------------------------------- 1 | namedten: 10 2 | namedfifty: 50 3 | 4 | % 5 | 6 | 0,1,=A1+B1,A1+B1+C1 7 | 4,5,6,7 8 | 9 | % 10 | 11 | A1 = 0 12 | B1 = C1 13 | C1 = 1 14 | D1 = "A1+B1+C1" -------------------------------------------------------------------------------- /test/trellis/basic_evaluation.leaf: -------------------------------------------------------------------------------- 1 | 2 | % 3 | 4 | 1,, 5 | 2,, 6 | =A1+A2,, 7 | =A3+1,, 8 | =A1+A2+A3+A4+10,, 9 | 10 | % 11 | 12 | A1=1 13 | A2=2 14 | A3=3 15 | A4=4 16 | A5=20 -------------------------------------------------------------------------------- /test/trellis/function_invocation.leaf: -------------------------------------------------------------------------------- 1 | 2 | % 3 | 4 | 1,=concat("hello ","1","2"), 5 | 2,, 6 | =A1+A2,, 7 | =A3+1,, 8 | =A1+A2+A3+A4+10,, 9 | 10 | % 11 | 12 | B1="hello 12" 13 | A3=3 14 | A4=4 15 | A5=20 16 | -------------------------------------------------------------------------------- /test/trellis/inlined_function_invocation.leaf: -------------------------------------------------------------------------------- 1 | 2 | % 3 | 4 | 1,={x+y+z}(9,A1,A2) 5 | 2, 6 | 7 | % 8 | 9 | A1=1 10 | A2=2 11 | B1=12 12 | -------------------------------------------------------------------------------- /test/trellis/matrix_evaluation.leaf: -------------------------------------------------------------------------------- 1 | 2 | % 3 | 4 | 1, 5 | 2,=A1:A2 6 | , 7 | 8 | % 9 | 10 | B2=1 11 | B3=2 -------------------------------------------------------------------------------- /test/trellis/matrix_spill_conflict_error.leaf: -------------------------------------------------------------------------------- 1 | 2 | % 3 | 4 | 1,, 5 | 2,=A1:A2, 6 | =A1:B1,, 7 | 8 | % 9 | 10 | A1=1 11 | A2=2 12 | B2=1 13 | B3=2 14 | error(A3) = "Spill error" -------------------------------------------------------------------------------- /test/trellis/matrix_spill_error.leaf: -------------------------------------------------------------------------------- 1 | 2 | % 3 | 4 | 1,, 5 | 2,=A1:A2, 6 | ,A string, 7 | 8 | % 9 | 10 | error(B2) = "Spill error" 11 | B3 = "A string" -------------------------------------------------------------------------------- /test/trellis/returns_errors.leaf: -------------------------------------------------------------------------------- 1 | 2 | % 3 | 4 | =1,, 5 | ABC,=A99991000, 6 | =A1+A2,=B2, 7 | =A3+1,, 8 | 9 | % 10 | 11 | error(B2)="Invalid address [99990999 0]" 12 | error(B3)="Invalid address [99990999 0]" 13 | error(A3)="+ only works for Integers" 14 | error(A4)="+ only works for Integers" -------------------------------------------------------------------------------- /test/trellis/spillage_dependency.leaf: -------------------------------------------------------------------------------- 1 | 2 | % 3 | 4 | 1,,=A1:A3 5 | 2,, 6 | 3,=C2, 7 | =B3,=A3:A5, 8 | ,, 9 | ,, 10 | 11 | % 12 | 13 | B3=2 --------------------------------------------------------------------------------