62a8a0a9c0afc68590b893478c82eca8ad748fee
[plstackapi.git] / planetstack / core / static / shell / opencloud_shell.js
1 // TryMongo
2 //
3 // Copyright (c) 2009 Kyle Banker
4 // Licensed under the MIT Licence.
5 // http://www.opensource.org/licenses/mit-license.php
6
7 // Readline class to handle line input.
8 var ReadLine = function(options) {
9   this.options      = options || {};
10   this.htmlForInput = this.options.htmlForInput;
11   this.inputHandler = this.options.handler || this.mockHandler;
12   this.scoper       = this.options.scoper;
13   this.terminal     = $(this.options.terminalId || "#terminal");
14   this.lineClass    = this.options.lineClass || '.readLine';
15   this.history      = [];
16   this.historyPtr   = 0;
17
18   this.initialize();
19 };
20
21 ReadLine.prototype = {
22
23   initialize: function() {
24     this.addInputLine();
25   },
26
27   // Enter a new input line with proper behavior.
28   addInputLine: function(stackLevel) {
29     stackLevel = stackLevel || 0;
30     this.terminal.append(this.htmlForInput(stackLevel));
31     var ctx = this;
32     ctx.activeLine = $(this.lineClass + '.active');
33
34     // Bind key events for entering and navigting history.
35     ctx.activeLine.bind("keydown", function(ev) {
36       switch (ev.keyCode) {
37         case EnterKeyCode:
38           ctx.processInput(this.value); 
39           break;
40         case UpArrowKeyCode: 
41           ctx.getCommand('previous');
42           break;
43         case DownArrowKeyCode: 
44           ctx.getCommand('next');
45           break;
46       }
47     });
48
49     this.activeLine.focus();
50   },
51
52   // Returns the 'next' or 'previous' command in this history.
53   getCommand: function(direction) {
54     if(this.history.length === 0) {
55       return;
56     }
57     this.adjustHistoryPointer(direction);
58     this.activeLine[0].value = this.history[this.historyPtr];
59     $(this.activeLine[0]).focus();
60     //this.activeLine[0].value = this.activeLine[0].value;
61   },
62
63   // Moves the history pointer to the 'next' or 'previous' position. 
64   adjustHistoryPointer: function(direction) {
65     if(direction == 'previous') {
66       if(this.historyPtr - 1 >= 0) {
67         this.historyPtr -= 1;
68       }
69     }
70     else {
71       if(this.historyPtr + 1 < this.history.length) {
72         this.historyPtr += 1;
73       }
74     }
75   },
76
77   // Return the handler's response.
78   processInput: function(value) {
79     var response = this.inputHandler.apply(this.scoper, [value]);
80     this.insertResponse(response.result);
81
82     // Save to the command history...
83     if((lineValue = value.trim()) !== "") {
84       this.history.push(lineValue);
85       this.historyPtr = this.history.length;
86     }
87
88     // deactivate the line...
89     this.activeLine.value = "";
90     this.activeLine.attr({disabled: true});
91     this.activeLine.removeClass('active');
92
93     // and add add a new command line.
94     this.addInputLine(response.stack);
95   },
96
97   insertResponse: function(response) {
98     if((response.length < 1) || (response=='"donotprintme"') || (response=='donotprintme')) {
99       this.activeLine.parent().append("<p class='response'></p>");
100     }
101     else {
102       this.activeLine.parent().append("<p class='response'>" + response + "</p>");
103     }
104   },
105
106   // Simply return the entered string if the user hasn't specified a smarter handler.
107   mockHandler: function(inputString) {
108     return function() {
109       this._process = function() { return inputString; };
110     };
111   }
112 };
113
114 var MongoHandler = function() {
115   this._currentCommand = "";
116   this._rawCommand     = "";
117   this._commandStack   = 0;
118   this._tutorialPtr    = 0;
119   this._tutorialMax    = 4;
120
121   this._mongo          = {};
122   this._mongo.test     = [];
123   this.collections     = [];
124 };
125
126 MongoHandler.prototype = {
127
128   _process: function(inputString, errorCheck) {
129     this._rawCommand += ' ' + inputString;
130
131     try {
132       inputString += '  '; // fixes certain bugs with the tokenizer.
133       var tokens    = inputString.tokens();
134       var mongoFunc = this._getCommand(tokens);
135       if(this._commandStack === 0 && inputString.match(/^\s*$/)) {
136         return {stack: 0, result: ''};
137       }
138       else if(this._commandStack === 0 && mongoFunc) {
139         this._resetCurrentCommand();
140         return {stack: 0, result: mongoFunc.apply(this, [tokens])};
141       }
142       else {
143         return this._evaluator(tokens);
144       }
145     }
146
147     catch(err) {
148         this._resetCurrentCommand();
149         console.trace();
150         return {stack: 0, result: "JS Error: " + err};
151     }
152   },
153
154   // Calls eval on the input string when ready.
155   _evaluator: function(tokens) {
156     isAssignment = tokens.length>=2 && tokens[0].type=="name" && tokens[1].type=="operator" && tokens[1].value=="=";
157
158     this._currentCommand += " " + this._massageTokens(tokens);
159     if(this._shouldEvaluateCommand(tokens))  {
160         xos = new OpenCloud();
161         print = this.print;
162
163         // So this eval statement is the heart of the REPL.
164         var result = eval(this._currentCommand.trim());
165         if(result === undefined) {
166           throw('result is undefined');
167         } else if (typeof(result) === 'function') {
168           throw('result is a function. did you mean to call it?');
169         } else {
170           result = $htmlFormat(result);
171         }
172         this._resetCurrentCommand();
173         if (isAssignment) {
174             return {stack: this._commandStack, result: ""};
175         } else {
176             return {stack: this._commandStack, result: result};
177         }
178       }
179
180     else {
181       return {stack: this._commandStack, result: ""};
182     }
183   },
184
185   _resetCurrentCommand: function() {
186     this._currentCommand = '';
187     this._rawCommand     = '';
188   },
189
190   // Evaluate only when we've exited any blocks.
191   _shouldEvaluateCommand: function(tokens) {
192     for(var i=0; i < tokens.length; i++) {
193       var token = tokens[i];
194       if(token.type == 'operator') {
195         if(token.value == '(' || token.value == '{') {
196           this._commandStack += 1;
197         }
198         else if(token.value == ')' || token.value == '}') {
199           this._commandStack -= 1;
200         }
201       }
202     }
203
204     if(this._commandStack === 0) {
205       return true;
206     }
207     else {
208       return false;
209     }
210   },
211
212   _massageTokens: function(tokens) {
213     for(var i=0; i < tokens.length; i++) {
214       if(tokens[i].type == 'name') {
215         if(tokens[i].value == 'var') {
216           tokens[i].value = '';
217         }
218       }
219     }
220     return this._collectTokens(tokens);
221   },
222
223   // Collects tokens into a string, placing spaces between variables.
224   // This methods is called after we scope the vars.
225   _collectTokens: function(tokens) {
226     var result = "";
227     for(var i=0; i < tokens.length; i++) {
228       if(tokens[i].type == "name" && tokens[i+1] && tokens[i+1].type == 'name') {
229         result += tokens[i].value + ' ';
230       }
231       else if (tokens[i].type == 'string') {
232         result += "'" + tokens[i].value + "'";
233       }
234       else {
235         result += tokens[i].value;
236       }
237     }
238     return result;
239   },
240
241   // print output to the screen, e.g., in a loop
242   // TODO: remove dependency here
243   print: function() {
244    $('.readLine.active').parent().append('<p>' + JSON.stringify(arguments[0]) + '</p>');
245    return "donotprintme";
246   },
247
248   /* MongoDB     */
249   /* ________________________________________ */
250
251   // help command
252   _help: function() {
253       return PTAG('HELP') +
254              PTAG('xos                                 list xos API object types') +
255              PTAG('xos.slices                          list methods to can call on slices') +
256              PTAG('xos.slices.all()                    get all slices') +
257              PTAG('xos.slices.filter({key: "value"})   filter using dictionary') +
258              PTAG('xos.slices.get({key: "value"})      get using dictionary')
259
260   },
261
262   _tutorial: function() {
263     this._tutorialPtr = 0;
264     return PTAG("This is a self-guided tutorial on the xos shell.") +
265            PTAG("The tutorial is simple, more or less a few basic commands to try.") +
266            PTAG("To go directly to any part tutorial, enter one of the commands t0, t1, t2...t10") +
267            PTAG("Otherwise, use 'next' and 'back'. Start by typing 'next' and pressing enter.");
268   },
269
270   // go to the next step in the tutorial.
271   _next: function() {
272     if(this._tutorialPtr < this._tutorialMax) {
273       return this['_t' + (this._tutorialPtr + 1)]();
274     }
275     else {
276       return "You've reached the end of the tutorial. To go to the beginning, type 'tutorial'";
277     }
278   },
279
280   // go to the previous step in the tutorial.
281   _back: function() {
282     if(this._tutorialPtr > 1) {
283       return this['_t' + (this._tutorialPtr - 1)]();
284     }
285     else {
286       return this._tutorial();
287     }
288   },
289
290   _t1: function() {
291     this._tutorialPtr = 1;
292     return PTAG('1. JavaScript Shell') +
293            PTAG('The first thing to notice is that the MongoDB shell is JavaScript-based.') +
294            PTAG('So you can do things like:') +
295            PTAG('  a = 5; ') +
296            PTAG('  a * 10; ') +
297            PTAG('  print(a); ') +
298            PTAG("  for(i=0; i<10; i++) { print('hello'); }; ") +
299            PTAG("Try a few JS commands; when you're ready to move on, enter 'next'");
300
301   },
302
303   _t2: function() {
304     this._tutorialPtr = 2;
305     return PTAG('2. List some objects') +
306            PTAG('Try these:') +
307            PTAG('    xos.slices.all();') +
308            PTAG('    xos.slivers.all();') +
309            PTAG('    xos.sites.all();') +
310            PTAG('    xos.nodes.all();');
311
312   },
313
314   _t3: function() {
315     this._tutorialPtr = 3;
316     return PTAG('3. Filter some objects') +
317            PTAG('Try these:') +
318            PTAG('    xos.slices.get({name: "HyperCache"});');
319            PTAG('    xos.nodes.filter({site_id: xos.sites.get({name: "Princeton"})["id"]});');
320
321   },
322
323   _t4: function() {
324     this._tutorialPtr = 4;
325     return PTAG('4. Available xos objects and methods') +
326            PTAG('Try these:') +
327            PTAG('    xos;') +
328            PTAG('    xos.nodes;');
329
330   },
331
332   _getCommand: function(tokens) {
333     if(tokens[0] && ArrayInclude(MongoKeywords,(tokens[0].value + '').toLowerCase())) {
334       switch(tokens[0].value.toLowerCase()) {
335         case 'help':
336           return this._help;
337
338         case 'tutorial':
339           return this._tutorial;
340         case 'next':
341           return this._next;
342         case 'back':
343           return this._back;
344         case 't0':
345           return this._tutorial;
346         case 't1':
347           return this._t1;
348         case 't2':
349           return this._t2;
350         case 't3':
351           return this._t3;
352         case 't4':
353           return this._t4;
354       }
355     }
356   }
357 };
358
359 function replaceAll(find, replace, str) {
360   return str.replace(new RegExp(find, 'g'), replace);\r
361 }
362
363 /* stackoverflow: http://stackoverflow.com/questions/4810841/how-can-i-pretty-print-json-using-javascript */
364 function syntaxHighlight(json) {
365     if ( json.hasOwnProperty("__str__")) {
366         return syntaxHighlight(json.__str__());
367     }
368     if (typeof json != 'string') {\r
369          json = JSON.stringify(json, undefined, "\t");\r
370     }\r
371     json = json.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');\r
372     return json.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function (match) {\r
373         var cls = 'terminal_number';\r
374         if (/^"/.test(match)) {\r
375             if (/:$/.test(match)) {\r
376                 cls = 'terminal_key';\r
377             } else {\r
378                 cls = 'terminal_string';\r
379             }\r
380         } else if (/true|false/.test(match)) {\r
381             cls = 'terminal_boolean';\r
382         } else if (/null/.test(match)) {\r
383             cls = 'terminal_null';\r
384         }\r
385         return '<span class="' + cls + '">' + match + '</span>';\r
386     });\r
387 }
388
389 $htmlFormat = function(obj) {
390   //JSON.stringify(obj,undefined,2)
391   result=replaceAll("\t","&nbsp;",replaceAll("\n","<br>",syntaxHighlight(obj))); //tojson(obj, ' ', ' ', true);
392   return result;
393 }
394
395 function startTerminal() {
396   var mongo       = new MongoHandler();
397   var terminal    = new ReadLine({htmlForInput: DefaultInputHtml,
398                                   handler: mongo._process,
399                                   scoper: mongo});
400   $("#terminal_help1").show();
401   $("#terminal_help2").show();
402   $("#terminal_wait").hide();
403
404   $("#terminal").bind('click', function() { $(".readLine.active").focus(); });
405 };
406
407 $(document).ready(function() {
408     updateOpenCloud(onLoaded = startTerminal);
409 });