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