3 // Copyright (c) 2009 Kyle Banker
4 // Licensed under the MIT Licence.
5 // http://www.opensource.org/licenses/mit-license.php
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';
21 ReadLine.prototype = {
23 initialize: function() {
27 // Enter a new input line with proper behavior.
28 addInputLine: function(stackLevel) {
29 stackLevel = stackLevel || 0;
30 this.terminal.append(this.htmlForInput(stackLevel));
32 ctx.activeLine = $(this.lineClass + '.active');
34 // Bind key events for entering and navigting history.
35 ctx.activeLine.bind("keydown", function(ev) {
38 ctx.processInput(this.value);
41 ctx.getCommand('previous');
43 case DownArrowKeyCode:
44 ctx.getCommand('next');
49 this.activeLine.focus();
52 // Returns the 'next' or 'previous' command in this history.
53 getCommand: function(direction) {
54 if(this.history.length === 0) {
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;
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) {
71 if(this.historyPtr + 1 < this.history.length) {
77 // Return the handler's response.
78 processInput: function(value) {
79 var response = this.inputHandler.apply(this.scoper, [value]);
80 this.insertResponse(response.result);
82 // Save to the command history...
83 if((lineValue = value.trim()) !== "") {
84 this.history.push(lineValue);
85 this.historyPtr = this.history.length;
88 // deactivate the line...
89 this.activeLine.value = "";
90 this.activeLine.attr({disabled: true});
91 this.activeLine.removeClass('active');
93 // and add add a new command line.
94 this.addInputLine(response.stack);
97 insertResponse: function(response) {
98 if((response.length < 1) || (response=='"donotprintme"') || (response=='donotprintme')) {
99 this.activeLine.parent().append("<p class='response'></p>");
102 this.activeLine.parent().append("<p class='response'>" + response + "</p>");
106 // Simply return the entered string if the user hasn't specified a smarter handler.
107 mockHandler: function(inputString) {
109 this._process = function() { return inputString; };
114 var MongoHandler = function() {
115 this._currentCommand = "";
116 this._rawCommand = "";
117 this._commandStack = 0;
118 this._tutorialPtr = 0;
119 this._tutorialMax = 4;
122 this._mongo.test = [];
123 this.collections = [];
126 MongoHandler.prototype = {
128 _process: function(inputString, errorCheck) {
129 this._rawCommand += ' ' + inputString;
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: ''};
138 else if(this._commandStack === 0 && mongoFunc) {
139 this._resetCurrentCommand();
140 return {stack: 0, result: mongoFunc.apply(this, [tokens])};
143 return this._evaluator(tokens);
148 this._resetCurrentCommand();
150 return {stack: 0, result: "JS Error: " + err};
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=="=";
158 this._currentCommand += " " + this._massageTokens(tokens);
159 if(this._shouldEvaluateCommand(tokens)) {
162 // So this eval statement is the heart of the REPL.
163 var result = eval(this._currentCommand.trim());
164 if(result === undefined) {
165 throw('result is undefined');
166 } else if (typeof(result) === 'function') {
167 throw('result is a function. did you mean to call it?');
169 result = $htmlFormat(result);
171 this._resetCurrentCommand();
173 return {stack: this._commandStack, result: ""};
175 return {stack: this._commandStack, result: result};
180 return {stack: this._commandStack, result: ""};
184 _resetCurrentCommand: function() {
185 this._currentCommand = '';
186 this._rawCommand = '';
189 // Evaluate only when we've exited any blocks.
190 _shouldEvaluateCommand: function(tokens) {
191 for(var i=0; i < tokens.length; i++) {
192 var token = tokens[i];
193 if(token.type == 'operator') {
194 if(token.value == '(' || token.value == '{') {
195 this._commandStack += 1;
197 else if(token.value == ')' || token.value == '}') {
198 this._commandStack -= 1;
203 if(this._commandStack === 0) {
211 _massageTokens: function(tokens) {
212 for(var i=0; i < tokens.length; i++) {
213 if(tokens[i].type == 'name') {
214 if(tokens[i].value == 'var') {
215 tokens[i].value = '';
219 return this._collectTokens(tokens);
222 // Collects tokens into a string, placing spaces between variables.
223 // This methods is called after we scope the vars.
224 _collectTokens: function(tokens) {
226 for(var i=0; i < tokens.length; i++) {
227 if(tokens[i].type == "name" && tokens[i+1] && tokens[i+1].type == 'name') {
228 result += tokens[i].value + ' ';
230 else if (tokens[i].type == 'string') {
231 result += "'" + tokens[i].value + "'";
234 result += tokens[i].value;
240 // print output to the screen, e.g., in a loop
241 // TODO: remove dependency here
243 $('.readLine.active').parent().append('<p>' + JSON.stringify(arguments[0]) + '</p>');
244 return "donotprintme";
248 /* ________________________________________ */
252 return PTAG('HELP') +
253 PTAG('xos list xos API object types') +
254 PTAG('xos.slices.fetch() fetch slices from the server') +
255 PTAG('xos.slices get the slices that were fetched');
259 _tutorial: function() {
260 this._tutorialPtr = 0;
261 return PTAG("This is a self-guided tutorial on the xos shell.") +
262 PTAG("The tutorial is simple, more or less a few basic commands to try.") +
263 PTAG("To go directly to any part tutorial, enter one of the commands t0, t1, t2...t10") +
264 PTAG("Otherwise, use 'next' and 'back'. Start by typing 'next' and pressing enter.");
267 // go to the next step in the tutorial.
269 if(this._tutorialPtr < this._tutorialMax) {
270 return this['_t' + (this._tutorialPtr + 1)]();
273 return "You've reached the end of the tutorial. To go to the beginning, type 'tutorial'";
277 // go to the previous step in the tutorial.
279 if(this._tutorialPtr > 1) {
280 return this['_t' + (this._tutorialPtr - 1)]();
283 return this._tutorial();
288 this._tutorialPtr = 1;
289 return PTAG('1. JavaScript Shell') +
290 PTAG('The first thing to notice is that the MongoDB shell is JavaScript-based.') +
291 PTAG('So you can do things like:') +
294 PTAG(' print(a); ') +
295 PTAG(" for(i=0; i<10; i++) { print('hello'); }; ") +
296 PTAG("Try a few JS commands; when you're ready to move on, enter 'next'");
301 this._tutorialPtr = 2;
302 return PTAG('2. Reading from the server is asynchronous') +
304 PTAG(' xos.slices.models;') +
305 PTAG(' // the above should have printed empty list') +
306 PTAG(' xos.slices.fetch();') +
307 PTAG(' // wait a second or two...') +
308 PTAG(' xos.slices.models;');
313 this._tutorialPtr = 3;
314 return PTAG('3. Responding to events') +
316 PTAG(' xos.slices.fetch();') +
317 PTAG(' tmp=xos.slices.on("change", function() { alert("woot!"); });') +
318 PTAG(' xos.slices.models[0].set("description", "somerandomtext");');
323 this._tutorialPtr = 4;
324 return PTAG('4. Available xos objects and methods') +
326 PTAG(' xos.listObjects();') +
327 PTAG(' xos.slices.listMethods();');
331 _getCommand: function(tokens) {
332 if(tokens[0] && ArrayInclude(MongoKeywords,(tokens[0].value + '').toLowerCase())) {
333 switch(tokens[0].value.toLowerCase()) {
338 return this._tutorial;
344 return this._tutorial;
358 function replaceAll(find, replace, str) {
359 return str.replace(new RegExp(find, 'g'), replace);
\r
362 /* stackoverflow: http://stackoverflow.com/questions/4810841/how-can-i-pretty-print-json-using-javascript */
363 function syntaxHighlight(json) {
364 if ( json.hasOwnProperty("__str__")) {
365 return syntaxHighlight(json.__str__());
367 if (typeof json != 'string') {
\r
368 json = JSON.stringify(json, undefined, "\t");
\r
370 json = json.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
\r
371 return json.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function (match) {
\r
372 var cls = 'terminal_number';
\r
373 if (/^"/.test(match)) {
\r
374 if (/:$/.test(match)) {
\r
375 cls = 'terminal_key';
\r
377 cls = 'terminal_string';
\r
379 } else if (/true|false/.test(match)) {
\r
380 cls = 'terminal_boolean';
\r
381 } else if (/null/.test(match)) {
\r
382 cls = 'terminal_null';
\r
384 return '<span class="' + cls + '">' + match + '</span>';
\r
388 $htmlFormat = function(obj) {
389 //JSON.stringify(obj,undefined,2)
390 result=replaceAll("\t"," ",replaceAll("\n","<br>",syntaxHighlight(obj))); //tojson(obj, ' ', ' ', true);
394 function startTerminal() {
395 var mongo = new MongoHandler();
396 var terminal = new ReadLine({htmlForInput: DefaultInputHtml,
397 handler: mongo._process,
399 $("#terminal_help1").show();
400 $("#terminal_help2").show();
401 $("#terminal_wait").hide();
403 $("#terminal").bind('click', function() { $(".readLine.active").focus(); });
406 $(document).ready(function() {