1// Demo.js -- Demonstrate some of the features of ShellInABox 2// Copyright (C) 2008-2009 Markus Gutschke <markus@shellinabox.com> 3// 4// This program is free software; you can redistribute it and/or modify 5// it under the terms of the GNU General Public License version 2 as 6// published by the Free Software Foundation. 7// 8// This program is distributed in the hope that it will be useful, 9// but WITHOUT ANY WARRANTY; without even the implied warranty of 10// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11// GNU General Public License for more details. 12// 13// You should have received a copy of the GNU General Public License along 14// with this program; if not, write to the Free Software Foundation, Inc., 15// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 16// 17// In addition to these license terms, the author grants the following 18// additional rights: 19// 20// If you modify this program, or any covered work, by linking or 21// combining it with the OpenSSL project's OpenSSL library (or a 22// modified version of that library), containing parts covered by the 23// terms of the OpenSSL or SSLeay licenses, the author 24// grants you additional permission to convey the resulting work. 25// Corresponding Source for a non-source form of such a combination 26// shall include the source code for the parts of OpenSSL used as well 27// as that of the covered work. 28// 29// You may at your option choose to remove this additional permission from 30// the work, or from any part of it. 31// 32// It is possible to build this program in a way that it loads OpenSSL 33// libraries at run-time. If doing so, the following notices are required 34// by the OpenSSL and SSLeay licenses: 35// 36// This product includes software developed by the OpenSSL Project 37// for use in the OpenSSL Toolkit. (http://www.openssl.org/) 38// 39// This product includes cryptographic software written by Eric Young 40// (eay@cryptsoft.com) 41// 42// 43// The most up-to-date version of this program is always available from 44// http://shellinabox.com 45// 46// 47// Notes: 48// 49// The author believes that for the purposes of this license, you meet the 50// requirements for publishing the source code, if your web server publishes 51// the source in unmodified form (i.e. with licensing information, comments, 52// formatting, and identifier names intact). If there are technical reasons 53// that require you to make changes to the source code when serving the 54// JavaScript (e.g to remove pre-processor directives from the source), these 55// changes should be done in a reversible fashion. 56// 57// The author does not consider websites that reference this script in 58// unmodified form, and web servers that serve this script in unmodified form 59// to be derived works. As such, they are believed to be outside of the 60// scope of this license and not subject to the rights or restrictions of the 61// GNU General Public License. 62// 63// If in doubt, consult a legal professional familiar with the laws that 64// apply in your country. 65 66#define STATE_IDLE 0 67#define STATE_INIT 1 68#define STATE_PROMPT 2 69#define STATE_READLINE 3 70#define STATE_COMMAND 4 71#define STATE_EXEC 5 72#define STATE_NEW_Y_N 6 73 74#define TYPE_STRING 0 75#define TYPE_NUMBER 1 76 77function extend(subClass, baseClass) { 78 function inheritance() { } 79 inheritance.prototype = baseClass.prototype; 80 subClass.prototype = new inheritance(); 81 subClass.prototype.constructor = subClass; 82 subClass.prototype.superClass = baseClass.prototype; 83}; 84 85function Demo(container) { 86 this.superClass.constructor.call(this, container); 87 this.gotoState(STATE_INIT); 88}; 89extend(Demo, VT100); 90 91Demo.prototype.keysPressed = function(ch) { 92 if (this.state == STATE_EXEC) { 93 for (var i = 0; i < ch.length; i++) { 94 var c = ch.charAt(i); 95 if (c == '\u0003') { 96 this.keys = ''; 97 this.error('Interrupted'); 98 return; 99 } 100 } 101 } 102 this.keys += ch; 103 this.gotoState(this.state); 104}; 105 106Demo.prototype.gotoState = function(state, tmo) { 107 this.state = state; 108 if (!this.timer || tmo) { 109 if (!tmo) { 110 tmo = 1; 111 } 112 this.nextTimer = setTimeout(function(demo) { 113 return function() { 114 demo.demo(); 115 }; 116 }(this), tmo); 117 } 118}; 119 120Demo.prototype.demo = function() { 121 var done = false; 122 this.nextTimer = undefined; 123 while (!done) { 124 var state = this.state; 125 this.state = STATE_PROMPT; 126 switch (state) { 127 case STATE_INIT: 128 done = this.doInit(); 129 break; 130 case STATE_PROMPT: 131 done = this.doPrompt(); 132 break; 133 case STATE_READLINE: 134 done = this.doReadLine(); 135 break; 136 case STATE_COMMAND: 137 done = this.doCommand(); 138 break; 139 case STATE_EXEC: 140 done = this.doExec(); 141 break; 142 case STATE_NEW_Y_N: 143 done = this.doNewYN(); 144 break; 145 default: 146 done = true; 147 break; 148 } 149 } 150 this.timer = this.nextTimer; 151 this.nextTimer = undefined; 152}; 153 154Demo.prototype.ok = function() { 155 this.vt100('OK\r\n'); 156 this.gotoState(STATE_PROMPT); 157}; 158 159Demo.prototype.error = function(msg) { 160 if (msg == undefined) { 161 msg = 'Syntax Error'; 162 } 163 this.printUnicode((this.cursorX != 0 ? '\r\n' : '') + '\u0007? ' + msg + 164 (this.currentLineIndex >= 0 ? ' in line ' + 165 this.program[this.evalLineIndex].lineNumber() : 166 '') + '\r\n'); 167 this.gotoState(STATE_PROMPT); 168 this.currentLineIndex = -1; 169 this.evalLineIndex = -1; 170 return undefined; 171}; 172 173Demo.prototype.doInit = function() { 174 this.vars = new Object(); 175 this.program = new Array(); 176 this.printUnicode( 177 '\u001Bc\u001B[34;4m' + 178 'ShellInABox Demo Script\u001B[24;31m\r\n' + 179 '\r\n' + 180 'Copyright 2009 by Markus Gutschke <markus@shellinabox.com>\u001B[0m\r\n' + 181 '\r\n' + 182 '\r\n' + 183 'This script simulates a minimal BASIC interpreter, allowing you to\r\n' + 184 'experiment with the JavaScript terminal emulator that is part of\r\n' + 185 'the ShellInABox project.\r\n' + 186 '\r\n' + 187 'Type HELP for a list of commands.\r\n' + 188 '\r\n'); 189 this.gotoState(STATE_PROMPT); 190 return false; 191}; 192 193Demo.prototype.doPrompt = function() { 194 this.keys = ''; 195 this.line = ''; 196 this.currentLineIndex = -1; 197 this.evalLineIndex = -1; 198 this.vt100((this.cursorX != 0 ? '\r\n' : '') + '> '); 199 this.gotoState(STATE_READLINE); 200 return false; 201}; 202 203Demo.prototype.printUnicode = function(s) { 204 var out = ''; 205 for (var i = 0; i < s.length; i++) { 206 var c = s.charAt(i); 207 if (c < '\x0080') { 208 out += c; 209 } else { 210 var c = s.charCodeAt(i); 211 if (c < 0x800) { 212 out += String.fromCharCode(0xC0 + (c >> 6) ) + 213 String.fromCharCode(0x80 + ( c & 0x3F)); 214 } else if (c < 0x10000) { 215 out += String.fromCharCode(0xE0 + (c >> 12) ) + 216 String.fromCharCode(0x80 + ((c >> 6) & 0x3F)) + 217 String.fromCharCode(0x80 + ( c & 0x3F)); 218 } else if (c < 0x110000) { 219 out += String.fromCharCode(0xF0 + (c >> 18) ) + 220 String.fromCharCode(0x80 + ((c >> 12) & 0x3F)) + 221 String.fromCharCode(0x80 + ((c >> 6) & 0x3F)) + 222 String.fromCharCode(0x80 + ( c & 0x3F)); 223 } 224 } 225 } 226 this.vt100(out); 227}; 228 229Demo.prototype.doReadLine = function() { 230 this.gotoState(STATE_READLINE); 231 var keys = this.keys; 232 this.keys = ''; 233 for (var i = 0; i < keys.length; i++) { 234 var ch = keys.charAt(i); 235 if (ch == '\u0008' || ch == '\u007F') { 236 if (this.line.length > 0) { 237 this.line = this.line.substr(0, this.line.length - 1); 238 if (this.cursorX == 0) { 239 var x = this.terminalWidth - 1; 240 var y = this.cursorY - 1; 241 this.gotoXY(x, y); 242 this.vt100(' '); 243 this.gotoXY(x, y); 244 } else { 245 this.vt100('\u0008 \u0008'); 246 } 247 } else { 248 this.vt100('\u0007'); 249 } 250 } else if (ch >= ' ') { 251 this.line += ch; 252 this.printUnicode(ch); 253 } else if (ch == '\r' || ch == '\n') { 254 this.vt100('\r\n'); 255 this.gotoState(STATE_COMMAND); 256 return false; 257 } else if (ch == '\u001B') { 258 // This was probably a function key. Just eat all of the following keys. 259 break; 260 } 261 } 262 return true; 263}; 264 265Demo.prototype.doCommand = function() { 266 this.gotoState(STATE_PROMPT); 267 var tokens = new this.Tokens(this.line); 268 this.line = ''; 269 var cmd = tokens.nextToken(); 270 if (cmd) { 271 cmd = cmd; 272 if (cmd.match(/^[0-9]+$/)) { 273 tokens.removeLineNumber(); 274 var lineNumber = parseInt(cmd); 275 var index = this.findLine(lineNumber); 276 if (tokens.nextToken() == null) { 277 if (index > 0) { 278 // Delete line from program 279 this.program.splice(index, 1); 280 } 281 } else { 282 tokens.reset(); 283 if (index >= 0) { 284 // Replace line in program 285 this.program[index].setTokens(tokens); 286 } else { 287 // Add new line to program 288 this.program.splice(-index - 1, 0, 289 new this.Line(lineNumber, tokens)); 290 } 291 } 292 } else { 293 this.currentLineIndex = -1; 294 this.evalLineIndex = -1; 295 tokens.reset(); 296 this.tokens = tokens; 297 return this.doEval(); 298 } 299 } 300 return false; 301}; 302 303Demo.prototype.doEval = function() { 304 var token = this.tokens.peekToken(); 305 if (token == "DIM") { 306 this.tokens.consume(); 307 this.doDim(); 308 } else if (token == "END") { 309 this.tokens.consume(); 310 this.doEnd(); 311 } else if (token == "GOTO") { 312 this.tokens.consume(); 313 this.doGoto(); 314 } else if (token == "HELP") { 315 this.tokens.consume(); 316 if (this.tokens.nextToken() != undefined) { 317 this.error('HELP does not take any arguments'); 318 } else { 319 this.vt100('Supported commands:\r\n' + 320 'DIM END GOTO HELP LET LIST NEW PRINT RUN\r\n'+ 321 '\r\n'+ 322 'Supported functions:\r\n'+ 323 'ABS() ASC() ATN() CHR$() COS() EXP() INT() LEFT$() LEN()\r\n'+ 324 'LOG() MID$() POS() RIGHT$() RND() SGN() SIN() SPC() SQR()\r\n'+ 325 'STR$() TAB() TAN() TI VAL()\r\n'); 326 } 327 } else if (token == "LET") { 328 this.tokens.consume(); 329 this.doAssignment(); 330 } else if (token == "LIST") { 331 this.tokens.consume(); 332 this.doList(); 333 } else if (token == "NEW") { 334 this.tokens.consume(); 335 if (this.tokens.nextToken() != undefined) { 336 this.error('NEW does not take any arguments'); 337 } else if (this.currentLineIndex >= 0) { 338 this.error('Cannot call NEW from a program'); 339 } else if (this.program.length == 0) { 340 this.ok(); 341 } else { 342 this.vt100('Do you really want to delete the program (y/N) '); 343 this.gotoState(STATE_NEW_Y_N); 344 } 345 } else if (token == "PRINT" || token == "?") { 346 this.tokens.consume(); 347 this.doPrint(); 348 } else if (token == "RUN") { 349 this.tokens.consume(); 350 if (this.tokens.nextToken() != null) { 351 this.error('RUN does not take any parameters'); 352 } else if (this.program.length > 0) { 353 this.currentLineIndex = 0; 354 this.vars = new Object(); 355 this.gotoState(STATE_EXEC); 356 } else { 357 this.ok(); 358 } 359 } else { 360 this.doAssignment(); 361 } 362 return false; 363}; 364 365Demo.prototype.arrayIndex = function() { 366 var token = this.tokens.peekToken(); 367 var arr = ''; 368 if (token == '(') { 369 this.tokens.consume(); 370 do { 371 var idx = this.expr(); 372 if (idx == undefined) { 373 return idx; 374 } else if (idx.type() != TYPE_NUMBER) { 375 return this.error('Numeric value expected'); 376 } 377 idx = Math.floor(idx.val()); 378 if (idx < 0) { 379 return this.error('Indices have to be positive'); 380 } 381 arr += ',' + idx; 382 token = this.tokens.nextToken(); 383 } while (token == ','); 384 if (token != ')') { 385 return this.error('")" expected'); 386 } 387 } 388 return arr; 389}; 390 391Demo.prototype.toInt = function(v) { 392 if (v < 0) { 393 return -Math.floor(-v); 394 } else { 395 return Math.floor( v); 396 } 397}; 398 399Demo.prototype.doAssignment = function() { 400 var id = this.tokens.nextToken(); 401 if (!id || !id.match(/^[A-Za-z][A-Za-z0-9_]*$/)) { 402 return this.error('Identifier expected'); 403 } 404 var token = this.tokens.peekToken(); 405 var isString = false; 406 var isInt = false; 407 if (token == '$') { 408 isString = true; 409 this.tokens.consume(); 410 } else if (token == '%') { 411 isInt = true; 412 this.tokens.consume(); 413 } 414 var arr = this.arrayIndex(); 415 if (arr == undefined) { 416 return arr; 417 } 418 token = this.tokens.nextToken(); 419 if (token != '=') { 420 return this.error('"=" expected'); 421 } 422 var value = this.expr(); 423 if (value == undefined) { 424 return value; 425 } 426 if (isString) { 427 if (value.type() != TYPE_STRING) { 428 return this.error('String expected'); 429 } 430 this.vars['str_' + id + arr] = value; 431 } else { 432 if (value.type() != TYPE_NUMBER) { 433 return this.error('Numeric value expected'); 434 } 435 if (isInt) { 436 value = this.toInt(value.val()); 437 value = new this.Value(TYPE_NUMBER, '' + value, value); 438 this.vars['int_' + id + arr] = value; 439 } else { 440 this.vars['var_' + id + arr] = value; 441 } 442 } 443}; 444 445Demo.prototype.doDim = function() { 446 for (;;) { 447 var token = this.tokens.nextToken(); 448 if (token == undefined) { 449 return; 450 } 451 if (!token || !token.match(/^[A-Za-z][A-Za-z0-9_]*$/)) { 452 return this.error('Identifier expected'); 453 } 454 token = this.tokens.nextToken(); 455 if (token == '$' || token == '%') { 456 token = this.tokens.nextToken(); 457 } 458 if (token != '(') { 459 return this.error('"(" expected'); 460 } 461 do { 462 var size = this.expr(); 463 if (!size) { 464 return size; 465 } 466 if (size.type() != TYPE_NUMBER) { 467 return this.error('Numeric value expected'); 468 } 469 if (Math.floor(size.val()) < 1) { 470 return this.error('Range error'); 471 } 472 token = this.tokens.nextToken(); 473 } while (token == ','); 474 if (token != ')') { 475 return this.error('")" expected'); 476 } 477 if (this.tokens.peekToken() != ',') { 478 break; 479 } 480 this.tokens.consume(); 481 } 482 if (this.tokens.peekToken() != undefined) { 483 return this.error(); 484 } 485}; 486 487Demo.prototype.doEnd = function() { 488 if (this.evalLineIndex < 0) { 489 return this.error('Cannot use END interactively'); 490 } 491 if (this.tokens.nextToken() != undefined) { 492 return this.error('END does not take any arguments'); 493 } 494 this.currentLineIndex = this.program.length; 495}; 496 497Demo.prototype.doGoto = function() { 498 if (this.evalLineIndex < 0) { 499 return this.error('Cannot use GOTO interactively'); 500 } 501 var value = this.expr(); 502 if (value == undefined) { 503 return; 504 } 505 if (value.type() != TYPE_NUMBER) { 506 return this.error('Numeric value expected'); 507 } 508 if (this.tokens.nextToken() != undefined) { 509 return this.error('GOTO takes exactly one numeric argument'); 510 } 511 var number = this.toInt(value.val()); 512 if (number <= 0) { 513 return this.error('Range error'); 514 } 515 var idx = this.findLine(number); 516 if (idx < 0) { 517 return this.error('No line number ' + line); 518 } 519 this.currentLineIndex = idx; 520}; 521 522Demo.prototype.doList = function() { 523 var start = undefined; 524 var stop = undefined; 525 var token = this.tokens.nextToken(); 526 if (token) { 527 if (token != '-' && !token.match(/[0-9]+/)) { 528 return this.error('LIST can optional take a start and stop line number'); 529 } 530 if (token != '-') { 531 start = parseInt(token); 532 token = this.tokens.nextToken(); 533 } 534 if (!token) { 535 stop = start; 536 } else { 537 if (token != '-') { 538 return this.error('Dash expected'); 539 } 540 token = this.tokens.nextToken(); 541 if (token) { 542 if (!token.match(/[0-9]+/)) { 543 return this.error( 544 'LIST can optionally take a start and stop line number'); 545 } 546 stop = parseInt(token); 547 if (start && stop < start) { 548 return this.error('Start line number has to come before stop'); 549 } 550 } 551 if (this.tokens.peekToken()) { 552 return this.error('Unexpected trailing arguments'); 553 } 554 } 555 } 556 557 var listed = false; 558 for (var i = 0; i < this.program.length; i++) { 559 var line = this.program[i]; 560 var lineNumber = line.lineNumber(); 561 if (start != undefined && start > lineNumber) { 562 continue; 563 } 564 if (stop != undefined && stop < lineNumber) { 565 break; 566 } 567 568 listed = true; 569 this.vt100('' + line.lineNumber() + ' '); 570 line.tokens().reset(); 571 var space = true; 572 var id = false; 573 for (var token; (token = line.tokens().nextToken()) != null; ) { 574 switch (token) { 575 case '=': 576 case '+': 577 case '-': 578 case '*': 579 case '/': 580 case '\\': 581 case '^': 582 this.vt100((space ? '' : ' ') + token + ' '); 583 space = true; 584 id = false; 585 break; 586 case '(': 587 case ')': 588 case '$': 589 case '%': 590 case '#': 591 this.vt100(token); 592 space = false; 593 id = false; 594 break; 595 case ',': 596 case ';': 597 case ':': 598 this.vt100(token + ' '); 599 space = true; 600 id = false; 601 break; 602 case '?': 603 token = 'PRINT'; 604 // fall thru 605 default: 606 this.printUnicode((id ? ' ' : '') + token); 607 space = false; 608 id = true; 609 break; 610 } 611 } 612 this.vt100('\r\n'); 613 } 614 if (!listed) { 615 this.ok(); 616 } 617}; 618 619Demo.prototype.doPrint = function() { 620 var tokens = this.tokens; 621 var last = undefined; 622 for (var token; (token = tokens.peekToken()); ) { 623 last = token; 624 if (token == ',') { 625 this.vt100('\t'); 626 tokens.consume(); 627 } else if (token == ';') { 628 // Do nothing 629 tokens.consume(); 630 } else { 631 var value = this.expr(); 632 if (value == undefined) { 633 return; 634 } 635 this.printUnicode(value.toString()); 636 } 637 } 638 if (last != ';') { 639 this.vt100('\r\n'); 640 } 641}; 642 643Demo.prototype.doExec = function() { 644 this.evalLineIndex = this.currentLineIndex++; 645 this.tokens = this.program[this.evalLineIndex].tokens(); 646 this.tokens.reset(); 647 this.doEval(); 648 if (this.currentLineIndex < 0) { 649 return false; 650 } else if (this.currentLineIndex >= this.program.length) { 651 this.currentLineIndex = -1; 652 this.ok(); 653 return false; 654 } else { 655 this.gotoState(STATE_EXEC, 20); 656 return true; 657 } 658}; 659 660Demo.prototype.doNewYN = function() { 661 for (var i = 0; i < this.keys.length; ) { 662 var ch = this.keys.charAt(i++); 663 if (ch == 'n' || ch == 'N' || ch == '\r' || ch == '\n') { 664 this.vt100('N\r\n'); 665 this.keys = this.keys.substr(i); 666 this.error('Aborted'); 667 return false; 668 } else if (ch == 'y' || ch == 'Y') { 669 this.vt100('Y\r\n'); 670 this.vars = new Object(); 671 this.program.splice(0, this.program.length); 672 this.keys = this.keys.substr(i); 673 this.ok(); 674 return false; 675 } else { 676 this.vt100('\u0007'); 677 } 678 } 679 this.gotoState(STATE_NEW_Y_N); 680 return true; 681}; 682 683Demo.prototype.findLine = function(lineNumber) { 684 var l = 0; 685 var h = this.program.length; 686 while (h > l) { 687 var m = Math.floor((l + h) / 2); 688 var n = this.program[m].lineNumber(); 689 if (n == lineNumber) { 690 return m; 691 } else if (n > lineNumber) { 692 h = m; 693 } else { 694 l = m + 1; 695 } 696 } 697 return -l - 1; 698}; 699 700Demo.prototype.expr = function() { 701 var value = this.term(); 702 while (value) { 703 var token = this.tokens.peekToken(); 704 if (token != '+' && token != '-') { 705 break; 706 } 707 this.tokens.consume(); 708 var v = this.term(); 709 if (!v) { 710 return v; 711 } 712 if (value.type() != v.type()) { 713 if (value.type() != TYPE_STRING) { 714 value = new this.Value(TYPE_STRING, ''+value.val(), ''+value.val()); 715 } 716 if (v.type() != TYPE_STRING) { 717 v = new this.Value(TYPE_STRING, ''+v.val(), ''+v.val()); 718 } 719 } 720 if (token == '-') { 721 if (value.type() == TYPE_STRING) { 722 return this.error('Cannot subtract strings'); 723 } 724 v = value.val() - v.val(); 725 } else { 726 v = value.val() + v.val(); 727 } 728 if (v == NaN) { 729 return this.error('Numeric range error'); 730 } 731 value = new this.Value(value.type(), ''+v, v); 732 } 733 return value; 734}; 735 736Demo.prototype.term = function() { 737 var value = this.expn(); 738 while (value) { 739 var token = this.tokens.peekToken(); 740 if (token != '*' && token != '/' && token != '\\') { 741 break; 742 } 743 this.tokens.consume(); 744 var v = this.expn(); 745 if (!v) { 746 return v; 747 } 748 if (value.type() != TYPE_NUMBER || v.type() != TYPE_NUMBER) { 749 return this.error('Cannot multiply or divide strings'); 750 } 751 if (token == '*') { 752 v = value.val() * v.val(); 753 } else { 754 v = value.val() / v.val(); 755 if (token == '\\') { 756 v = this.toInt(v); 757 } 758 } 759 if (v == NaN) { 760 return this.error('Numeric range error'); 761 } 762 value = new this.Value(TYPE_NUMBER, ''+v, v); 763 } 764 return value; 765}; 766 767Demo.prototype.expn = function() { 768 var value = this.intrinsic(); 769 var token = this.tokens.peekToken(); 770 if (token == '^') { 771 this.tokens.consume(); 772 var exp = this.intrinsic(); 773 if (exp == undefined || exp.val() == NaN) { 774 return exp; 775 } 776 if (value.type() != TYPE_NUMBER || exp.type() != TYPE_NUMBER) { 777 return this.error("Numeric value expected"); 778 } 779 var v = Math.pow(value.val(), exp.val()); 780 value = new this.Value(TYPE_NUMBER, '' + v, v); 781 } 782 return value; 783}; 784 785Demo.prototype.intrinsic = function() { 786 var token = this.tokens.peekToken(); 787 var args = undefined; 788 var value, v, fnc, arg1, arg2, arg3; 789 if (!token) { 790 return this.error('Unexpected end of input'); 791 } else if (token.match(/^(?:ABS|ASC|ATN|CHR\$|COS|EXP|INT|LEN|LOG|POS|RND|SGN|SIN|SPC|SQR|STR\$|TAB|TAN|VAL)$/)) { 792 fnc = token; 793 args = 1; 794 } else if (token.match(/^(?:LEFT\$|RIGHT\$)$/)) { 795 fnc = token; 796 args = 2; 797 } else if (token == 'MID$') { 798 fnc = token; 799 args = 3; 800 } else if (token == 'TI') { 801 this.tokens.consume(); 802 v = (new Date()).getTime() / 1000.0; 803 return new this.Value(TYPE_NUMBER, '' + v, v); 804 } else { 805 return this.factor(); 806 } 807 this.tokens.consume(); 808 token = this.tokens.nextToken(); 809 if (token != '(') { 810 return this.error('"(" expected'); 811 } 812 arg1 = this.expr(); 813 if (!arg1) { 814 return arg1; 815 } 816 token = this.tokens.nextToken(); 817 if (--args) { 818 if (token != ',') { 819 return this.error('"," expected'); 820 } 821 arg2 = this.expr(); 822 if (!arg2) { 823 return arg2; 824 } 825 token = this.tokens.nextToken(); 826 if (--args) { 827 if (token != ',') { 828 return this.error('"," expected'); 829 } 830 arg3 = this.expr(); 831 if (!arg3) { 832 return arg3; 833 } 834 token = this.tokens.nextToken(); 835 } 836 } 837 if (token != ')') { 838 return this.error('")" expected'); 839 } 840 switch (fnc) { 841 case 'ASC': 842 if (arg1.type() != TYPE_STRING || arg1.val().length < 1) { 843 return this.error('Non-empty string expected'); 844 } 845 v = arg1.val().charCodeAt(0); 846 value = new this.Value(TYPE_NUMBER, '' + v, v); 847 break; 848 case 'LEN': 849 if (arg1.type() != TYPE_STRING) { 850 return this.error('String expected'); 851 } 852 v = arg1.val().length; 853 value = new this.Value(TYPE_NUMBER, '' + v, v); 854 break; 855 case 'LEFT$': 856 if (arg1.type() != TYPE_STRING || arg2.type() != TYPE_NUMBER || 857 arg2.type() < 0) { 858 return this.error('Invalid arguments'); 859 } 860 v = arg1.val().substr(0, Math.floor(arg2.val())); 861 value = new this.Value(TYPE_STRING, v, v); 862 break; 863 case 'MID$': 864 if (arg1.type() != TYPE_STRING || arg2.type() != TYPE_NUMBER || 865 arg3.type() != TYPE_NUMBER || arg2.val() < 0 || arg3.val() < 0) { 866 return this.error('Invalid arguments'); 867 } 868 v = arg1.val().substr(Math.floor(arg2.val()), 869 Math.floor(arg3.val())); 870 value = new this.Value(TYPE_STRING, v, v); 871 break; 872 case 'RIGHT$': 873 if (arg1.type() != TYPE_STRING || arg2.type() != TYPE_NUMBER || 874 arg2.type() < 0) { 875 return this.error('Invalid arguments'); 876 } 877 v = Math.floor(arg2.val()); 878 if (v > arg1.val().length) { 879 v = arg1.val().length; 880 } 881 v = arg1.val().substr(arg1.val().length - v); 882 value = new this.Value(TYPE_STRING, v, v); 883 break; 884 case 'STR$': 885 value = new this.Value(TYPE_STRING, arg1.toString(), 886 arg1.toString()); 887 break; 888 case 'VAL': 889 if (arg1.type() == TYPE_NUMBER) { 890 value = arg1; 891 } else { 892 if (arg1.val().match(/^[0-9]+$/)) { 893 v = parseInt(arg1.val()); 894 } else { 895 v = parseFloat(arg1.val()); 896 } 897 value = new this.Value(TYPE_NUMBER, '' + v, v); 898 } 899 break; 900 default: 901 if (arg1.type() != TYPE_NUMBER) { 902 return this.error('Numeric value expected'); 903 } 904 switch (fnc) { 905 case 'CHR$': 906 if (arg1.val() < 0 || arg1.val() > 65535) { 907 return this.error('Invalid Unicode range'); 908 } 909 v = String.fromCharCode(arg1.val()); 910 value = new this.Value(TYPE_STRING, v, v); 911 break; 912 case 'SPC': 913 if (arg1.val() < 0) { 914 return this.error('Range error'); 915 } 916 v = arg1.val() >= 1 ? 917 '\u001B[' + Math.floor(arg1.val()) + 'C' : ''; 918 value = new this.Value(TYPE_STRING, v, v); 919 break; 920 case 'TAB': 921 if (arg1.val() < 0) { 922 return this.error('Range error'); 923 } 924 v = '\r' + (arg1.val() >= 1 ? 925 '\u001B[' + (Math.floor(arg1.val())*8) + 'C' : ''); 926 value = new this.Value(TYPE_STRING, v, v); 927 break; 928 default: 929 switch (fnc) { 930 case 'ABS': v = Math.abs(arg1.val()); break; 931 case 'ATN': v = Math.atan(arg1.val()); break; 932 case 'COS': v = Math.cos(arg1.val()); break; 933 case 'EXP': v = Math.exp(arg1.val()); break; 934 case 'INT': v = Math.floor(arg1.val()); break; 935 case 'LOG': v = Math.log(arg1.val()); break; 936 case 'POS': v = this.cursorX; break; 937 case 'SGN': v = arg1.val() < 0 ? -1 : arg1.val() ? 1 : 0; break; 938 case 'SIN': v = Math.sin(arg1.val()); break; 939 case 'SQR': v = Math.sqrt(arg1.val()); break; 940 case 'TAN': v = Math.tan(arg1.val()); break; 941 case 'RND': 942 if (this.prng == undefined) { 943 this.prng = 1013904223; 944 } 945 if (arg1.type() == TYPE_NUMBER && arg1.val() < 0) { 946 this.prng = Math.floor(1664525*arg1.val()) & 0xFFFFFFFF; 947 } 948 if (arg1.type() != TYPE_NUMBER || arg1.val() != 0) { 949 this.prng = Math.floor(1664525*this.prng + 1013904223) & 950 0xFFFFFFFF; 951 } 952 v = ((this.prng & 0x7FFFFFFF) / 65536.0) / 32768; 953 break; 954 } 955 value = new this.Value(TYPE_NUMBER, '' + v, v); 956 } 957 } 958 if (v == NaN) { 959 return this.error('Numeric range error'); 960 } 961 return value; 962}; 963 964Demo.prototype.factor = function() { 965 var token = this.tokens.nextToken(); 966 var value; 967 if (token == '-') { 968 value = this.expr(); 969 if (!value) { 970 return value; 971 } 972 if (value.type() != TYPE_NUMBER) { 973 return this.error('Numeric value expected'); 974 } 975 return new this.Value(TYPE_NUMBER, '' + -value.val(), -value.val()); 976 } 977 if (!token) { 978 return this.error(); 979 } 980 if (token == '(') { 981 value = this.expr(); 982 token = this.tokens.nextToken(); 983 if (token != ')' && value != undefined) { 984 return this.error('")" expected'); 985 } 986 } else { 987 var str; 988 if ((str = token.match(/^"(.*)"/)) != null) { 989 value = new this.Value(TYPE_STRING, str[1], str[1]); 990 } else if (token.match(/^[0-9]/)) { 991 var number; 992 if (token.match(/^[0-9]*$/)) { 993 number = parseInt(token); 994 } else { 995 number = parseFloat(token); 996 } 997 if (number == NaN) { 998 return this.error('Numeric range error'); 999 } 1000 value = new this.Value(TYPE_NUMBER, token, number); 1001 } else if (token.match(/^[A-Za-z][A-Za-z0-9_]*$/)) { 1002 if (this.tokens.peekToken() == '$') { 1003 this.tokens.consume(); 1004 var arr= this.arrayIndex(); 1005 if (arr == undefined) { 1006 return arr; 1007 } 1008 value = this.vars['str_' + token + arr]; 1009 if (value == undefined) { 1010 value= new this.Value(TYPE_STRING, '', ''); 1011 } 1012 } else { 1013 var n = 'var_'; 1014 if (this.tokens.peekToken() == '%') { 1015 this.tokens.consume(); 1016 n = 'int_'; 1017 } 1018 var arr= this.arrayIndex(); 1019 if (arr == undefined) { 1020 return arr; 1021 } 1022 value = this.vars[n + token + arr]; 1023 if (value == undefined) { 1024 value= new this.Value(TYPE_NUMBER, '0', 0); 1025 } 1026 } 1027 } else { 1028 return this.error(); 1029 } 1030 } 1031 1032 return value; 1033}; 1034 1035Demo.prototype.Tokens = function(line) { 1036 this.line = line; 1037 this.tokens = line; 1038 this.len = undefined; 1039}; 1040 1041Demo.prototype.Tokens.prototype.peekToken = function() { 1042 this.len = undefined; 1043 this.tokens = this.tokens.replace(/^[ \t]*/, ''); 1044 var tokens = this.tokens; 1045 if (!tokens.length) { 1046 return null; 1047 } 1048 var token = tokens.charAt(0); 1049 switch (token) { 1050 case '<': 1051 if (tokens.length > 1) { 1052 if (tokens.charAt(1) == '>') { 1053 token = '<>'; 1054 } else if (tokens.charAt(1) == '=') { 1055 token = '<='; 1056 } 1057 } 1058 break; 1059 case '>': 1060 if (tokens.charAt(1) == '=') { 1061 token = '>='; 1062 } 1063 break; 1064 case '=': 1065 case '+': 1066 case '-': 1067 case '*': 1068 case '/': 1069 case '\\': 1070 case '^': 1071 case '(': 1072 case ')': 1073 case '?': 1074 case ',': 1075 case ';': 1076 case ':': 1077 case '$': 1078 case '%': 1079 case '#': 1080 break; 1081 case '"': 1082 token = tokens.match(/"((?:""|[^"])*)"/); // " 1083 if (!token) { 1084 token = undefined; 1085 } else { 1086 this.len = token[0].length; 1087 token = '"' + token[1].replace(/""/g, '"') + '"'; 1088 } 1089 break; 1090 default: 1091 if (token >= '0' && token <= '9' || token == '.') { 1092 token = tokens.match(/^[0-9]*(?:[.][0-9]*)?(?:[eE][-+]?[0-9]+)?/); 1093 if (!token) { 1094 token = undefined; 1095 } else { 1096 token = token[0]; 1097 } 1098 } else if (token >= 'A' && token <= 'Z' || 1099 token >= 'a' && token <= 'z') { 1100 token = tokens.match(/^(?:CHR\$|STR\$|LEFT\$|RIGHT\$|MID\$)/i); 1101 if (token) { 1102 token = token[0].toUpperCase(); 1103 } else { 1104 token = tokens.match(/^[A-Za-z][A-Za-z0-9_]*/); 1105 if (!token) { 1106 token = undefined; 1107 } else { 1108 token = token[0].toUpperCase(); 1109 } 1110 } 1111 } else { 1112 token = ''; 1113 } 1114 } 1115 1116 if (this.len == undefined) { 1117 if (token) { 1118 this.len = token.length; 1119 } else { 1120 this.len = 1; 1121 } 1122 } 1123 1124 return token; 1125}; 1126 1127Demo.prototype.Tokens.prototype.consume = function() { 1128 if (this.len) { 1129 this.tokens = this.tokens.substr(this.len); 1130 this.len = undefined; 1131 } 1132}; 1133 1134Demo.prototype.Tokens.prototype.nextToken = function() { 1135 var token = this.peekToken(); 1136 this.consume(); 1137 return token; 1138}; 1139 1140Demo.prototype.Tokens.prototype.removeLineNumber = function() { 1141 this.line = this.line.replace(/^[0-9]*[ \t]*/, ''); 1142}; 1143 1144Demo.prototype.Tokens.prototype.reset = function() { 1145 this.tokens = this.line; 1146}; 1147 1148Demo.prototype.Line = function(lineNumber, tokens) { 1149 this.lineNumber_ = lineNumber; 1150 this.tokens_ = tokens; 1151}; 1152 1153Demo.prototype.Line.prototype.lineNumber = function() { 1154 return this.lineNumber_; 1155}; 1156 1157Demo.prototype.Line.prototype.tokens = function() { 1158 return this.tokens_; 1159}; 1160 1161Demo.prototype.Line.prototype.setTokens = function(tokens) { 1162 this.tokens_ = tokens; 1163}; 1164 1165Demo.prototype.Line.prototype.sort = function(a, b) { 1166 return a.lineNumber_ - b.lineNumber_; 1167}; 1168 1169Demo.prototype.Value = function(type, str, val) { 1170 this.t = type; 1171 this.s = str; 1172 this.v = val; 1173}; 1174 1175Demo.prototype.Value.prototype.type = function() { 1176 return this.t; 1177}; 1178 1179Demo.prototype.Value.prototype.val = function() { 1180 return this.v; 1181}; 1182 1183Demo.prototype.Value.prototype.toString = function() { 1184 return this.s; 1185}; 1186