// Demo.js -- Demonstrate some of the features of ShellInABox // Copyright (C) 2008-2009 Markus Gutschke // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License version 2 as // published by the Free Software Foundation. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along // with this program; if not, write to the Free Software Foundation, Inc., // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. // // In addition to these license terms, the author grants the following // additional rights: // // If you modify this program, or any covered work, by linking or // combining it with the OpenSSL project's OpenSSL library (or a // modified version of that library), containing parts covered by the // terms of the OpenSSL or SSLeay licenses, the author // grants you additional permission to convey the resulting work. // Corresponding Source for a non-source form of such a combination // shall include the source code for the parts of OpenSSL used as well // as that of the covered work. // // You may at your option choose to remove this additional permission from // the work, or from any part of it. // // It is possible to build this program in a way that it loads OpenSSL // libraries at run-time. If doing so, the following notices are required // by the OpenSSL and SSLeay licenses: // // This product includes software developed by the OpenSSL Project // for use in the OpenSSL Toolkit. (http://www.openssl.org/) // // This product includes cryptographic software written by Eric Young // (eay@cryptsoft.com) // // // The most up-to-date version of this program is always available from // http://shellinabox.com // // // Notes: // // The author believes that for the purposes of this license, you meet the // requirements for publishing the source code, if your web server publishes // the source in unmodified form (i.e. with licensing information, comments, // formatting, and identifier names intact). If there are technical reasons // that require you to make changes to the source code when serving the // JavaScript (e.g to remove pre-processor directives from the source), these // changes should be done in a reversible fashion. // // The author does not consider websites that reference this script in // unmodified form, and web servers that serve this script in unmodified form // to be derived works. As such, they are believed to be outside of the // scope of this license and not subject to the rights or restrictions of the // GNU General Public License. // // If in doubt, consult a legal professional familiar with the laws that // apply in your country. #define STATE_IDLE 0 #define STATE_INIT 1 #define STATE_PROMPT 2 #define STATE_READLINE 3 #define STATE_COMMAND 4 #define STATE_EVAL 5 #define STATE_EXEC 6 #define STATE_NEW_Y_N 7 #define TYPE_STRING 0 #define TYPE_NUMBER 1 #define TYPE_VAR 2 #define TYPE_STRVAR 3 #define TYPE_INTVAR 4 #define TYPE_ARR 5 #define TYPE_STRARR 6 #define TYPE_INTARR 7 function extend(subClass, baseClass) { function inheritance() { } inheritance.prototype = baseClass.prototype; subClass.prototype = new inheritance(); subClass.prototype.constructor = subClass; subClass.prototype.superClass = baseClass.prototype; }; function Demo(container) { this.superClass.constructor.call(this, container); this.gotoState(STATE_INIT); }; extend(Demo, VT100); Demo.prototype.keysPressed = function(ch) { if (this.state == STATE_EXEC) { for (var i = 0; i < ch.length; i++) { var c = ch.charAt(i); if (c == '\u0003') { this.keys = ''; this.error('Interrupted'); return; } } } this.keys += ch; this.gotoState(this.state); }; Demo.prototype.gotoState = function(state, tmo) { this.state = state; if (!this.timer || tmo) { if (!tmo) { tmo = 1; } this.nextTimer = setTimeout(function(demo) { return function() { demo.demo(); }; }(this), tmo); } }; Demo.prototype.demo = function() { var done = false; this.nextTimer = undefined; while (!done) { var state = this.state; this.state = STATE_IDLE; switch (state) { case STATE_INIT: done = this.doInit(); break; case STATE_PROMPT: done = this.doPrompt(); break; case STATE_READLINE: done = this.doReadLine(); break; case STATE_COMMAND: done = this.doCommand(); break; case STATE_EVAL: done = this.doEval(); break; case STATE_EXEC: done = this.doExec(); break; case STATE_NEW_Y_N: done = this.doNewYN(); break; case STATE_IDLE: default: done = true; break; } } this.timer = this.nextTimer; this.nextTimer = undefined; }; Demo.prototype.ok = function() { this.vt100('OK\r\n'); this.gotoState(STATE_PROMPT); }; Demo.prototype.error = function(msg) { if (msg == undefined) { msg = 'Syntax Error'; } this.vt100((this.cursorX != 0 ? '\r\n' : '') + '\u0007? ' + msg + (this.currentLineIndex >= 0 ? ' in line ' + this.program[this.currentLineIndex].lineNumber() : '') + '\r\n'); this.gotoState(STATE_PROMPT); this.currentLineIndex = -1; return undefined; }; Demo.prototype.doInit = function() { this.program = new Array(); this.vt100( '\u001Bc\u001B[34;4m' + 'ShellInABox Demo Script\u001B[24;31m\r\n' + '\r\n' + 'Copyright 2009 by Markus Gutschke \u001B[0m\r\n' + '\r\n' + '\r\n' + 'This script simulates a minimal BASIC interpreter, allowing you to\r\n' + 'experiment with the JavaScript terminal emulator that is part of\r\n' + 'the ShellInABox project.\r\n' + '\r\n' + 'Type HELP for a list of commands.\r\n' + '\r\n'); this.gotoState(STATE_PROMPT); return false; }; Demo.prototype.doPrompt = function() { this.keys = ''; this.line = ''; this.currentLineIndex = -1; this.vt100((this.cursorX != 0 ? '\r\n' : '') + '> '); this.gotoState(STATE_READLINE); return false; }; Demo.prototype.doReadLine = function() { this.gotoState(STATE_READLINE); var keys = this.keys; this.keys = ''; for (var i = 0; i < keys.length; i++) { var ch = keys.charAt(i); if (ch >= ' ' && ch < '\u007F' || ch > '\u00A0') { this.line += ch; this.vt100(ch); } else if (ch == '\r' || ch == '\n') { this.vt100('\r\n'); this.gotoState(STATE_COMMAND); return false; } else if (ch == '\u0008' || ch == '\u007F') { if (this.line.length > 0) { this.line = this.line.substr(0, this.line.length - 1); if (this.cursorX == 0) { var x = this.terminalWidth - 1; var y = this.cursorY - 1; this.gotoXY(x, y); this.vt100(' '); this.gotoXY(x, y); } else { this.vt100('\u0008 \u0008'); } } else { this.vt100('\u0007'); } } else if (ch == '\u001B') { // This was probably a function key. Just eat all of the following keys. break; } } return true; }; Demo.prototype.doCommand = function() { this.gotoState(STATE_PROMPT); var tokens = new this.Tokens(this.line); this.line = ''; var cmd = tokens.nextToken(); if (cmd) { cmd = cmd; if (cmd.match(/^[0-9]+$/)) { tokens.removeLineNumber(); var lineNumber = parseInt(cmd); var index = this.findLine(lineNumber); if (tokens.nextToken() == null) { if (index > 0) { // Delete line from program this.program.splice(index, 1); } } else { if (index >= 0) { // Replace line in program this.program[index].setTokens(tokens); } else { // Add new line to program this.program.splice(-index - 1, 0, new this.Line(lineNumber, tokens)); } } } else { this.currentLineIndex = -1; this.tokens = tokens; this.gotoState(STATE_EVAL); } } tokens.reset(); return false; }; Demo.prototype.doEval = function() { this.gotoState(STATE_PROMPT); var token = this.tokens.nextToken(); if (token == "HELP") { this.vt100('Supported commands:\r\n' + ' HELP LIST NEW PRINT RUN\r\n'); } else if (token == "LIST") { this.doList(); } else if (token == "NEW") { if (this.currentLineIndex >= 0) { this.error('Cannot call NEW from a program'); } else if (this.program.length == 0) { this.ok(); } else { this.vt100('Do you really want to delete the program (y/N) '); this.gotoState(STATE_NEW_Y_N); } } else if (token == "PRINT" || token == "?") { this.doPrint(); } else if (token == "RUN") { if (this.tokens.peekToken() != null) { this.error('RUN does not take any parameters'); } else if (this.program.length > 0) { this.currentLineIndex = 0; this.gotoState(STATE_EXEC); } else { this.ok(); } } else { this.error(token ? 'Unknown command: ' + token : undefined); } return false; }; Demo.prototype.doList = function() { var start = undefined; var stop = undefined; var token = this.tokens.nextToken(); if (token) { if (!token.match(/[0-9]+/)) { return this.error('LIST can optional take a start and stop line number'); } start = parseInt(token); token = this.tokens.nextToken(); if (token) { if (token != ',') { return this.error('Comma expected'); } token = this.tokens.nextToken(); if (!token || !token.match(/[0-9]+/)) { return this.error( 'LIST can optionally take a start and stop line number'); } stop = token.parseInt(token); if (stop < start) { return this.error('Start line number has to come before stop'); } } } for (var i = 0; i < this.program.length; i++) { var line = this.program[i]; var lineNumber = line.lineNumber(); if (start != undefined && start > lineNumber) { continue; } if (stop != undefined && stop < lineNumber) { break; } this.vt100('' + line.lineNumber() + ' '); line.tokens().reset(); var space = true; var id = false; for (var token; (token = line.tokens().nextToken()) != null; ) { switch (token) { case '=': case '+': case '-': case '*': case '/': case '\\': case '^': this.vt100((space ? '' : ' ') + token + ' '); space = true; id = false; break; case '(': case ')': case '?': case '$': case '%': case '#': this.vt100(token); space = false; id = false; break; case ',': case ';': case ':': this.vt100(token + ' '); space = true; id = false; break; default: this.vt100((id ? ' ' : '') + token); space = false; id = true; break; } } this.vt100('\r\n'); } }; Demo.prototype.doPrint = function() { var tokens = this.tokens; var last = undefined; for (var token; (token = tokens.peekToken()); ) { last = token; if (token == ',') { this.vt100('\t'); tokens.consume(); } else if (token == ';') { // Do nothing tokens.consume(); } else { var value = this.expr(); if (value == undefined) { return; } this.vt100(value.toString()); } } if (last != ';') { this.vt100('\r\n'); } }; Demo.prototype.doExec = function() { this.tokens = this.program[this.currentLineIndex++].tokens(); this.tokens.reset(); this.doEval(); if (this.currentLineIndex < 0) { return false; } else if (this.currentLineIndex >= this.program.length) { this.ok(); return false; } else { this.gotoState(STATE_EXEC, 20); return true; } }; Demo.prototype.doNewYN = function() { for (var i = 0; i < this.keys.length; ) { var ch = this.keys.charAt(i++); if (ch == 'n' || ch == 'N' || ch == '\r' || ch == '\n') { this.vt100('N\r\n'); this.keys = this.keys.substr(i); this.error('Aborted'); return false; } else if (ch == 'y' || ch == 'Y') { this.vt100('Y\r\n'); this.program.splice(0, this.program.length); this.keys = this.keys.substr(i); this.ok(); return false; } else { this.vt100('\u0007'); } } this.gotoState(STATE_NEW_Y_N); return true; }; Demo.prototype.findLine = function(lineNumber) { var l = 0; var h = this.program.length; while (h > l) { var m = Math.floor((l + h) / 2); var n = this.program[m].lineNumber(); if (n == lineNumber) { return m; } else if (n > lineNumber) { h = m; } else { l = m + 1; } } return -l - 1; }; Demo.prototype.expr = function() { var value = this.term(); while (value) { var token = this.tokens.peekToken(); if (token != '+' && token != '-') { break; } this.tokens.consume(); var v = this.term(); if (!v) { return v; } if (value.type() != v.type()) { if (value.type() != TYPE_STRING) { value = new this.Value(TYPE_STRING, ''+value.val(), ''+value.val()); } if (v.type() != TYPE_STRING) { v = new this.Value(TYPE_STRING, ''+v.val(), ''+v.val()); } } if (token == '-') { if (value.type() == TYPE_STRING) { return this.error('Cannot subtract strings'); } v = value.val() - v.val(); } else { v = value.val() + v.val(); } if (v == NaN) { return this.error('Numeric range error'); } value = new this.Value(value.type(), ''+v, v); } return value; }; Demo.prototype.term = function() { var value = this.factor(); while (value) { var token = this.tokens.peekToken(); if (token != '*' && token != '/' && token != '\\') { break; } this.tokens.consume(); var v = this.factor(); if (!v) { return v; } if (value.type() != TYPE_NUMBER || v.type() != TYPE_NUMBER) { return this.error('Cannot multiply or divide strings'); } if (token == '*') { v = value.val() * v.val(); } else { v = value.val() / v.val(); if (token == '\\') { if (v < 0) { v = -Math.floor(-v); } else { v = Math.floor( v); } } } if (v == NaN) { return this.error('Numeric range error'); } value = new this.Value(TYPE_NUMBER, ''+v, v); } return value; }; Demo.prototype.factor = function() { var token = this.tokens.nextToken(); if (!token) { return token; } var value = undefined; var str; if ((str = token.match(/^"(.*)"/)) != null) { value = new this.Value(TYPE_STRING, str[1], str[1]); } else if (token.match(/^[0-9]/)) { var number; if (token.match(/^[0-9]*$/)) { number = parseInt(token); } else { number = parseFloat(token); } if (number == NaN) { return this.error('Numeric range error'); } value = new this.Value(TYPE_NUMBER, token, number); } else { return this.error(); } return value; }; Demo.prototype.Tokens = function(line) { this.line = line; this.tokens = line; this.len = undefined; }; Demo.prototype.Tokens.prototype.peekToken = function() { this.len = undefined; this.tokens = this.tokens.replace(/^[ \t]*/, ''); var tokens = this.tokens; if (!tokens.length) { return null; } var token = tokens.charAt(0); switch (token) { case '<': if (tokens.length > 1) { if (tokens.charAt(1) == '>') { token = '<>'; } else if (tokens.charAt(1) == '=') { token = '<='; } } break; case '>': if (tokens.charAt(1) == '=') { token = '>='; } break; case '=': case '+': case '-': case '*': case '/': case '\\': case '^': case '(': case ')': case '?': case ',': case ';': case ':': case '$': case '%': case '#': break; case '"': token = tokens.match(/"((?:""|[^"])*)"/); // " if (!token) { token = undefined; } else { this.len = token[0].length; token = '"' + token[1].replace(/""/g, '"') + '"'; } break; default: if (token >= '0' && token <= '9' || token == '.') { token = tokens.match(/^[0-9]*(?:[.][0-9]*)?(?:[eE][-+]?[0-9]+)?/); if (!token) { token = undefined; } else { token = token[0]; } } else if (token >= 'A' && token <= 'Z' || token >= 'a' && token <= 'z') { token = tokens.match(/^[A-Za-z][A-Za-z0-9_]*/); if (!token) { token = undefined; } else { token = token[0].toUpperCase(); } } else { token = ''; } } if (this.len == undefined) { if (token) { this.len = token.length; } else { this.len = 1; } } return token; }; Demo.prototype.Tokens.prototype.consume = function() { if (this.len) { this.tokens = this.tokens.substr(this.len); this.len = undefined; } }; Demo.prototype.Tokens.prototype.nextToken = function() { var token = this.peekToken(); this.consume(); return token; }; Demo.prototype.Tokens.prototype.removeLineNumber = function() { this.line = this.line.replace(/^[0-9]*[ \t]*/, ''); }; Demo.prototype.Tokens.prototype.reset = function() { this.tokens = this.line; }; Demo.prototype.Line = function(lineNumber, tokens) { this.lineNumber_ = lineNumber; this.tokens_ = tokens; }; Demo.prototype.Line.prototype.lineNumber = function() { return this.lineNumber_; }; Demo.prototype.Line.prototype.tokens = function() { return this.tokens_; }; Demo.prototype.Line.prototype.setTokens = function(tokens) { this.tokens_ = tokens; }; Demo.prototype.Line.prototype.sort = function(a, b) { return a.lineNumber_ - b.lineNumber_; }; Demo.prototype.Value = function(type, str, val) { this.t = type; this.s = str; this.v = val; }; Demo.prototype.Value.prototype.type = function() { return this.t; }; Demo.prototype.Value.prototype.val = function() { return this.v; }; Demo.prototype.Value.prototype.toString = function() { return this.s; };