web-console-compat 3.5.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.markdown +110 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.markdown +5 -0
  5. data/Rakefile +27 -0
  6. data/lib/web-console-compat.rb +1 -0
  7. data/lib/web-console.rb +1 -0
  8. data/lib/web_console.rb +28 -0
  9. data/lib/web_console/context.rb +43 -0
  10. data/lib/web_console/errors.rb +7 -0
  11. data/lib/web_console/evaluator.rb +33 -0
  12. data/lib/web_console/exception_mapper.rb +33 -0
  13. data/lib/web_console/extensions.rb +44 -0
  14. data/lib/web_console/integration.rb +31 -0
  15. data/lib/web_console/integration/cruby.rb +23 -0
  16. data/lib/web_console/integration/rubinius.rb +39 -0
  17. data/lib/web_console/locales/en.yml +15 -0
  18. data/lib/web_console/middleware.rb +140 -0
  19. data/lib/web_console/railtie.rb +71 -0
  20. data/lib/web_console/request.rb +50 -0
  21. data/lib/web_console/response.rb +23 -0
  22. data/lib/web_console/session.rb +76 -0
  23. data/lib/web_console/tasks/extensions.rake +60 -0
  24. data/lib/web_console/tasks/templates.rake +54 -0
  25. data/lib/web_console/template.rb +23 -0
  26. data/lib/web_console/templates/_inner_console_markup.html.erb +8 -0
  27. data/lib/web_console/templates/_markup.html.erb +5 -0
  28. data/lib/web_console/templates/_prompt_box_markup.html.erb +2 -0
  29. data/lib/web_console/templates/console.js.erb +922 -0
  30. data/lib/web_console/templates/error_page.js.erb +70 -0
  31. data/lib/web_console/templates/index.html.erb +8 -0
  32. data/lib/web_console/templates/layouts/inlined_string.erb +1 -0
  33. data/lib/web_console/templates/layouts/javascript.erb +5 -0
  34. data/lib/web_console/templates/main.js.erb +1 -0
  35. data/lib/web_console/templates/style.css.erb +33 -0
  36. data/lib/web_console/testing/erb_precompiler.rb +25 -0
  37. data/lib/web_console/testing/fake_middleware.rb +39 -0
  38. data/lib/web_console/testing/helper.rb +9 -0
  39. data/lib/web_console/version.rb +3 -0
  40. data/lib/web_console/view.rb +50 -0
  41. data/lib/web_console/whiny_request.rb +31 -0
  42. data/lib/web_console/whitelist.rb +44 -0
  43. metadata +147 -0
@@ -0,0 +1,23 @@
1
+ module WebConsole
2
+ # A facade that handles template rendering and composition.
3
+ #
4
+ # It introduces template helpers to ease the inclusion of scripts only on
5
+ # Rails error pages.
6
+ class Template
7
+ # Lets you customize the default templates folder location.
8
+ cattr_accessor :template_paths
9
+ @@template_paths = [ File.expand_path('../templates', __FILE__) ]
10
+
11
+ def initialize(env, session)
12
+ @env = env
13
+ @session = session
14
+ @mount_point = Middleware.mount_point
15
+ end
16
+
17
+ # Render a template (inferred from +template_paths+) as a plain string.
18
+ def render(template)
19
+ view = View.new(template_paths, instance_values)
20
+ view.render(template: template, layout: false)
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,8 @@
1
+ <div class='resizer layer'></div>
2
+ <div class='console-outer layer'>
3
+ <div class='console-actions'>
4
+ <div class='close-button button' title='close'>x</div>
5
+ </div>
6
+ <div class='console-inner'></div>
7
+ </div>
8
+ <input class='clipboard' type='text'>
@@ -0,0 +1,5 @@
1
+ <div id="console"
2
+ data-mount-point='<%= @mount_point %>'
3
+ data-session-id='<%= @session.id %>'
4
+ data-prompt-label='>> '>
5
+ </div>
@@ -0,0 +1,2 @@
1
+ <span class='console-prompt-label'></span>
2
+ <pre class='console-prompt-display'></pre>
@@ -0,0 +1,922 @@
1
+ /**
2
+ * Constructor for command storage.
3
+ * It uses localStorage if available. Otherwise fallback to normal JS array.
4
+ */
5
+ function CommandStorage() {
6
+ this.previousCommands = [];
7
+ var previousCommandOffset = 0;
8
+ var hasLocalStorage = typeof window.localStorage !== 'undefined';
9
+ var STORAGE_KEY = "web_console_previous_commands";
10
+ var MAX_STORAGE = 100;
11
+
12
+ if (hasLocalStorage) {
13
+ this.previousCommands = JSON.parse(localStorage.getItem(STORAGE_KEY)) || [];
14
+ previousCommandOffset = this.previousCommands.length;
15
+ }
16
+
17
+ this.addCommand = function(command) {
18
+ previousCommandOffset = this.previousCommands.push(command);
19
+
20
+ if (previousCommandOffset > MAX_STORAGE) {
21
+ this.previousCommands.splice(0, 1);
22
+ previousCommandOffset = MAX_STORAGE;
23
+ }
24
+
25
+ if (hasLocalStorage) {
26
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(this.previousCommands));
27
+ }
28
+ };
29
+
30
+ this.navigate = function(offset) {
31
+ previousCommandOffset += offset;
32
+
33
+ if (previousCommandOffset < 0) {
34
+ previousCommandOffset = -1;
35
+ return null;
36
+ }
37
+
38
+ if (previousCommandOffset >= this.previousCommands.length) {
39
+ previousCommandOffset = this.previousCommands.length;
40
+ return null;
41
+ }
42
+
43
+ return this.previousCommands[previousCommandOffset];
44
+ }
45
+ }
46
+
47
+ function Autocomplete(_words, prefix) {
48
+ this.words = prepareWords(_words);
49
+ this.current = -1;
50
+ this.left = 0; // [left, right)
51
+ this.right = this.words.length;
52
+ this.confirmed = false;
53
+
54
+ function createSpan(label, className) {
55
+ var el = document.createElement('span');
56
+ addClass(el, className);
57
+ el.innerText = label;
58
+ return el;
59
+ }
60
+
61
+ function prepareWords(words) {
62
+ // convert into an object with priority and element
63
+ var res = new Array(words.length);
64
+ for (var i = 0, ind = 0; i < words.length; ++i) {
65
+ res[i] = new Array(words[i].length);
66
+ for (var j = 0; j < words[i].length; ++j) {
67
+ res[i][j] = {
68
+ word: words[i][j],
69
+ priority: i,
70
+ element: createSpan(words[i][j], 'trimmed keyword')
71
+ };
72
+ }
73
+ }
74
+ // flatten and sort by alphabetical order to refine incrementally
75
+ res = flatten(res);
76
+ res.sort(function(a, b) { return a.word == b.word ? 0 : (a.word < b.word ? -1 : 1); });
77
+ for (var i = 0; i < res.length; ++i) res[i].element.dataset.index = i;
78
+ return res;
79
+ }
80
+
81
+ this.view = document.createElement('pre');
82
+ addClass(this.view, 'auto-complete console-message');
83
+ this.view.appendChild(this.prefix = createSpan('...', 'trimmed keyword'));
84
+ this.view.appendChild(this.stage = document.createElement('span'));
85
+ this.elements = this.stage.children;
86
+ this.view.appendChild(this.suffix = createSpan('...', 'trimmed keyword'));
87
+
88
+ this.refine(prefix || '');
89
+ }
90
+
91
+ Autocomplete.prototype.getSelectedWord = function() {
92
+ return this.lastSelected && this.lastSelected.innerText;
93
+ };
94
+
95
+ Autocomplete.prototype.onFinished = function(callback) {
96
+ this.onFinishedCallback = callback;
97
+ if (this.confirmed) callback(this.confirmed);
98
+ };
99
+
100
+ Autocomplete.prototype.onKeyDown = function(ev) {
101
+ var self = this;
102
+ if (!this.elements.length) return;
103
+
104
+ function move(nextCurrent) {
105
+ if (self.lastSelected) removeClass(self.lastSelected, 'selected');
106
+ addClass(self.lastSelected = self.elements[nextCurrent], 'selected');
107
+ self.trim(self.current, true);
108
+ self.trim(nextCurrent, false);
109
+ self.current = nextCurrent;
110
+ }
111
+
112
+ switch (ev.keyCode) {
113
+ case 69:
114
+ if (ev.ctrlKey) {
115
+ move(this.current + 1 >= this.elements.length ? 0 : this.current + 1);
116
+ return true;
117
+ }
118
+ return false;
119
+ case 9: // Tab
120
+ if (ev.shiftKey) { // move back
121
+ move(this.current - 1 < 0 ? this.elements.length - 1 : this.current - 1);
122
+ } else { // move next
123
+ move(this.current + 1 >= this.elements.length ? 0 : this.current + 1);
124
+ }
125
+ return true;
126
+ case 13: // Enter
127
+ this.finish();
128
+ return true;
129
+ case 27: // Esc
130
+ this.cancel();
131
+ return true;
132
+ case 37: case 38: case 39: case 40: // disable using arrow keys on completing
133
+ return true;
134
+ }
135
+
136
+ return false;
137
+ };
138
+
139
+ Autocomplete.prototype.trim = function(from, needToTrim) {
140
+ var self = this;
141
+ var num = 5;
142
+
143
+ if (this.elements.length > num) {
144
+ (0 < from ? removeClass : addClass)(this.prefix, 'trimmed');
145
+ (from + num < this.elements.length ? removeClass : addClass)(this.suffix, 'trimmed');
146
+ } else {
147
+ addClass(this.prefix, 'trimmed');
148
+ addClass(this.suffix, 'trimmed');
149
+ }
150
+
151
+ function iterate(x) {
152
+ for (var i = 0; i < num; ++i, ++x) if (0 <= x && x < self.elements.length) {
153
+ toggleClass(self.elements[x], 'trimmed');
154
+ }
155
+ }
156
+
157
+ var toggleClass = needToTrim ? addClass : removeClass;
158
+ if (from < 0) {
159
+ iterate(0);
160
+ } else if (from + num - 1 >= this.elements.length) {
161
+ iterate(this.elements.length - num);
162
+ } else {
163
+ iterate(from);
164
+ }
165
+ };
166
+
167
+ Autocomplete.prototype.refine = function(prefix) {
168
+ if (this.confirmed) return;
169
+ var inc = !this.prev || (prefix.length >= this.prev.length);
170
+ this.prev = prefix;
171
+ var self = this;
172
+
173
+ function remove(parent, child) {
174
+ if (parent == child.parentNode) parent.removeChild(child);
175
+ }
176
+
177
+ function toggle(el) {
178
+ return inc ? remove(self.stage, el) : self.stage.appendChild(el);
179
+ }
180
+
181
+ function startsWith(str, prefix) {
182
+ return !prefix || str.substr(0, prefix.length) === prefix;
183
+ }
184
+
185
+ function moveRight(l, r) {
186
+ while (l < r && inc !== startsWith(self.words[l].word, prefix)) toggle(self.words[l++].element);
187
+ return l;
188
+ }
189
+
190
+ function moveLeft(l, r) {
191
+ while (l < r - 1 && inc !== startsWith(self.words[r-1].word, prefix)) toggle(self.words[--r].element);
192
+ return r;
193
+ }
194
+
195
+ self.trim(self.current, true); // reset trimming
196
+
197
+ // Refine the range of words having same prefix
198
+ if (inc) {
199
+ self.left = moveRight(self.left, self.right);
200
+ self.right = moveLeft(self.left, self.right);
201
+ } else {
202
+ self.left = moveLeft(-1, self.left);
203
+ self.right = moveRight(self.right, self.words.length);
204
+ }
205
+
206
+ // Render elements with sorting by scope groups
207
+ var words = this.words.slice(this.left, this.right);
208
+ words.sort(function(a, b) { return a.priority == b.priority ? (a.word < b.word ? -1 : 1) : (a.priority < b.priority ? -1 : 1); });
209
+ removeAllChildren(this.elements);
210
+ for (var i = 0; i < words.length; ++i) {
211
+ this.stage.appendChild(words[i].element);
212
+ }
213
+
214
+ // Keep a previous selected element if the refined range includes the element
215
+ if (this.lastSelected && this.left <= this.lastSelected.dataset.index && this.lastSelected.dataset.index < this.right) {
216
+ this.current = Array.prototype.indexOf.call(this.elements, this.lastSelected);
217
+ this.trim(this.current, false);
218
+ } else {
219
+ if (this.lastSelected) removeClass(this.lastSelected, 'selected');
220
+ this.lastSelected = null;
221
+ this.current = -1;
222
+ this.trim(0, false);
223
+ }
224
+
225
+ if (self.left + 1 == self.right) {
226
+ self.current = 0;
227
+ self.finish();
228
+ } else if (self.left == self.right) {
229
+ self.cancel();
230
+ }
231
+ };
232
+
233
+ Autocomplete.prototype.finish = function() {
234
+ if (0 <= this.current && this.current < this.elements.length) {
235
+ this.confirmed = this.elements[this.current].innerText;
236
+ if (this.onFinishedCallback) this.onFinishedCallback(this.confirmed);
237
+ this.removeView();
238
+ } else {
239
+ this.cancel();
240
+ }
241
+ };
242
+
243
+ Autocomplete.prototype.cancel = function() {
244
+ if (this.onFinishedCallback) this.onFinishedCallback();
245
+ this.removeView();
246
+ };
247
+
248
+ Autocomplete.prototype.removeView = function() {
249
+ if (this.view.parentNode) this.view.parentNode.removeChild(this.view);
250
+ removeAllChildren(this.view);
251
+ }
252
+
253
+ // HTML strings for dynamic elements.
254
+ var consoleInnerHtml = <%= render_inlined_string '_inner_console_markup.html' %>;
255
+ var promptBoxHtml = <%= render_inlined_string '_prompt_box_markup.html' %>;
256
+ // CSS
257
+ var consoleStyleCss = <%= render_inlined_string 'style.css' %>;
258
+ // Insert a style element with the unique ID
259
+ var styleElementId = 'sr02459pvbvrmhco';
260
+
261
+ // REPLConsole Constructor
262
+ function REPLConsole(config) {
263
+ function getConfig(key, defaultValue) {
264
+ return config && config[key] || defaultValue;
265
+ }
266
+
267
+ this.commandStorage = new CommandStorage();
268
+ this.prompt = getConfig('promptLabel', ' >>');
269
+ this.mountPoint = getConfig('mountPoint');
270
+ this.sessionId = getConfig('sessionId');
271
+ this.autocomplete = false;
272
+ }
273
+
274
+ REPLConsole.prototype.getSessionUrl = function(path) {
275
+ var parts = [ this.mountPoint, 'repl_sessions', this.sessionId ];
276
+ if (path) {
277
+ parts.push(path);
278
+ }
279
+ // Join and remove duplicate slashes.
280
+ return parts.join('/').replace(/([^:]\/)\/+/g, '$1');
281
+ };
282
+
283
+ REPLConsole.prototype.contextRequest = function(keyword, callback) {
284
+ putRequest(this.getSessionUrl(), 'context=' + getContext(keyword), function(xhr) {
285
+ if (xhr.status == 200) {
286
+ callback(null, JSON.parse(xhr.responseText));
287
+ } else {
288
+ callback(xhr.statusText);
289
+ }
290
+ });
291
+ };
292
+
293
+ REPLConsole.prototype.commandHandle = function(line, callback) {
294
+ var self = this;
295
+ var params = 'input=' + encodeURIComponent(line);
296
+ callback = callback || function() {};
297
+
298
+ function isSuccess(status) {
299
+ return status >= 200 && status < 300 || status === 304;
300
+ }
301
+
302
+ function parseJSON(text) {
303
+ try {
304
+ return JSON.parse(text);
305
+ } catch (e) {
306
+ return null;
307
+ }
308
+ }
309
+
310
+ function getErrorText(xhr) {
311
+ if (!xhr.status) {
312
+ return "Connection Refused";
313
+ } else {
314
+ return xhr.status + ' ' + xhr.statusText;
315
+ }
316
+ }
317
+
318
+ putRequest(self.getSessionUrl(), params, function(xhr) {
319
+ var response = parseJSON(xhr.responseText);
320
+ var result = isSuccess(xhr.status);
321
+ if (result) {
322
+ self.writeOutput(response.output);
323
+ } else {
324
+ if (response && response.output) {
325
+ self.writeError(response.output);
326
+ } else {
327
+ self.writeError(getErrorText(xhr));
328
+ }
329
+ }
330
+ callback(result, response);
331
+ });
332
+ };
333
+
334
+ REPLConsole.prototype.uninstall = function() {
335
+ this.container.parentNode.removeChild(this.container);
336
+ };
337
+
338
+ REPLConsole.prototype.install = function(container) {
339
+ var _this = this;
340
+
341
+ document.onkeydown = function(ev) {
342
+ if (_this.focused) { _this.onKeyDown(ev); }
343
+ };
344
+
345
+ document.onkeypress = function(ev) {
346
+ if (_this.focused) { _this.onKeyPress(ev); }
347
+ };
348
+
349
+ document.addEventListener('mousedown', function(ev) {
350
+ var el = ev.target || ev.srcElement;
351
+
352
+ if (el) {
353
+ do {
354
+ if (el === container) {
355
+ _this.focus();
356
+ return;
357
+ }
358
+ } while (el = el.parentNode);
359
+
360
+ _this.blur();
361
+ }
362
+ });
363
+
364
+ // Render the console.
365
+ container.innerHTML = consoleInnerHtml;
366
+
367
+ var consoleOuter = findChild(container, 'console-outer');
368
+ var consoleActions = findChild(consoleOuter, 'console-actions');
369
+
370
+ addClass(container, 'console');
371
+ addClass(container.getElementsByClassName('layer'), 'pos-absolute border-box');
372
+ addClass(container.getElementsByClassName('button'), 'border-box');
373
+ addClass(consoleActions, 'pos-fixed pos-right');
374
+
375
+ // Make the console resizable.
376
+ function resizeContainer(ev) {
377
+ var startY = ev.clientY;
378
+ var startHeight = parseInt(document.defaultView.getComputedStyle(container).height, 10);
379
+ var scrollTopStart = consoleOuter.scrollTop;
380
+ var clientHeightStart = consoleOuter.clientHeight;
381
+
382
+ var doDrag = function(e) {
383
+ container.style.height = (startHeight + startY - e.clientY) + 'px';
384
+ consoleOuter.scrollTop = scrollTopStart + (clientHeightStart - consoleOuter.clientHeight);
385
+ shiftConsoleActions();
386
+ };
387
+
388
+ var stopDrag = function(e) {
389
+ document.documentElement.removeEventListener('mousemove', doDrag, false);
390
+ document.documentElement.removeEventListener('mouseup', stopDrag, false);
391
+ };
392
+
393
+ document.documentElement.addEventListener('mousemove', doDrag, false);
394
+ document.documentElement.addEventListener('mouseup', stopDrag, false);
395
+ }
396
+
397
+ function closeContainer(ev) {
398
+ container.parentNode.removeChild(container);
399
+ }
400
+
401
+ var shifted = false;
402
+ function shiftConsoleActions() {
403
+ if (consoleOuter.scrollHeight > consoleOuter.clientHeight) {
404
+ var widthDiff = document.documentElement.clientWidth - consoleOuter.clientWidth;
405
+ if (shifted || ! widthDiff) return;
406
+ shifted = true;
407
+ consoleActions.style.marginRight = widthDiff + 'px';
408
+ } else if (shifted) {
409
+ shifted = false;
410
+ consoleActions.style.marginRight = '0px';
411
+ }
412
+ }
413
+
414
+ // Initialize
415
+ this.container = container;
416
+ this.outer = consoleOuter;
417
+ this.inner = findChild(this.outer, 'console-inner');
418
+ this.clipboard = findChild(container, 'clipboard');
419
+ this.suggestWait = 1500;
420
+ this.newPromptBox();
421
+ this.insertCss();
422
+
423
+ findChild(container, 'resizer').addEventListener('mousedown', resizeContainer);
424
+ findChild(consoleActions, 'close-button').addEventListener('click', closeContainer);
425
+ consoleOuter.addEventListener('DOMNodeInserted', shiftConsoleActions);
426
+
427
+ REPLConsole.currentSession = this;
428
+ };
429
+
430
+ // Add CSS styles dynamically. This probably doesnt work for IE <8.
431
+ REPLConsole.prototype.insertCss = function() {
432
+ if (document.getElementById(styleElementId)) {
433
+ return; // already inserted
434
+ }
435
+ var style = document.createElement('style');
436
+ style.type = 'text/css';
437
+ style.innerHTML = consoleStyleCss;
438
+ style.id = styleElementId;
439
+ document.getElementsByTagName('head')[0].appendChild(style);
440
+ };
441
+
442
+ REPLConsole.prototype.focus = function() {
443
+ if (! this.focused) {
444
+ this.focused = true;
445
+ if (! hasClass(this.inner, "console-focus")) {
446
+ addClass(this.inner, "console-focus");
447
+ }
448
+ this.scrollToBottom();
449
+ }
450
+ };
451
+
452
+ REPLConsole.prototype.blur = function() {
453
+ this.focused = false;
454
+ removeClass(this.inner, "console-focus");
455
+ };
456
+
457
+ /**
458
+ * Add a new empty prompt box to the console.
459
+ */
460
+ REPLConsole.prototype.newPromptBox = function() {
461
+ // Remove the caret from previous prompt display if any.
462
+ if (this.promptDisplay) {
463
+ this.removeCaretFromPrompt();
464
+ }
465
+
466
+ var promptBox = document.createElement('div');
467
+ promptBox.className = "console-prompt-box";
468
+ promptBox.innerHTML = promptBoxHtml;
469
+ this.promptLabel = promptBox.getElementsByClassName('console-prompt-label')[0];
470
+ this.promptDisplay = promptBox.getElementsByClassName('console-prompt-display')[0];
471
+ // Render the prompt box
472
+ this.setInput("");
473
+ this.promptLabel.innerHTML = this.prompt;
474
+ this.inner.appendChild(promptBox);
475
+ this.scrollToBottom();
476
+ };
477
+
478
+ /**
479
+ * Remove the caret from the prompt box,
480
+ * mainly before adding a new prompt box.
481
+ * For simplicity, just re-render the prompt box
482
+ * with caret position -1.
483
+ */
484
+ REPLConsole.prototype.removeCaretFromPrompt = function() {
485
+ this.setInput(this._input, -1);
486
+ };
487
+
488
+ REPLConsole.prototype.getSuggestion = function(keyword) {
489
+ var self = this;
490
+
491
+ function show(found) {
492
+ if (!found) return;
493
+ var hint = self.promptDisplay.childNodes[1];
494
+ hint.className = 'console-hint';
495
+ hint.dataset.keyword = found;
496
+ hint.innerText = found.substr(self.suggestKeyword.length);
497
+ // clear hinting information after timeout in a few time
498
+ if (self.suggestTimeout) clearTimeout(self.suggestTimeout);
499
+ self.suggestTimeout = setTimeout(function() { self.renderInput() }, self.suggestWait);
500
+ }
501
+
502
+ function find(context) {
503
+ var k = self.suggestKeyword;
504
+ for (var i = 0; i < context.length; ++i) if (context[i].substr(0, k.length) === k) {
505
+ if (context[i] === k) return;
506
+ return context[i];
507
+ }
508
+ }
509
+
510
+ function request(keyword, callback) {
511
+ self.contextRequest(keyword, function(err, res) {
512
+ if (err) throw new Error(err);
513
+ var c = flatten(res['context']);
514
+ c.sort();
515
+ callback(c);
516
+ });
517
+ }
518
+
519
+ self.suggestKeyword = keyword;
520
+ var input = getContext(keyword);
521
+ if (keyword.length - input.length < 3) return;
522
+
523
+ if (self.suggestInput !== input) {
524
+ self.suggestInput = input;
525
+ request(keyword, function(c) {
526
+ show(find(self.suggestContext = c));
527
+ });
528
+ } else if (self.suggestContext) {
529
+ show(find(self.suggestContext));
530
+ }
531
+ };
532
+
533
+ REPLConsole.prototype.getHintKeyword = function() {
534
+ var hint = this.promptDisplay.childNodes[1];
535
+ return hint.className === 'console-hint' && hint.dataset.keyword;
536
+ };
537
+
538
+ REPLConsole.prototype.setInput = function(input, caretPos) {
539
+ if (input == null) return; // keep value if input is undefined
540
+ this._caretPos = caretPos === undefined ? input.length : caretPos;
541
+ this._input = input;
542
+ if (this.autocomplete) this.autocomplete.refine(this.getCurrentWord());
543
+ this.renderInput();
544
+ if (!this.autocomplete && input.length == this._caretPos) this.getSuggestion(this.getCurrentWord());
545
+ };
546
+
547
+ /**
548
+ * Add some text to the existing input.
549
+ */
550
+ REPLConsole.prototype.addToInput = function(val, caretPos) {
551
+ caretPos = caretPos || this._caretPos;
552
+ var before = this._input.substring(0, caretPos);
553
+ var after = this._input.substring(caretPos, this._input.length);
554
+ var newInput = before + val + after;
555
+ this.setInput(newInput, caretPos + val.length);
556
+ };
557
+
558
+ /**
559
+ * Render the input prompt. This is called whenever
560
+ * the user input changes, sometimes not very efficient.
561
+ */
562
+ REPLConsole.prototype.renderInput = function() {
563
+ // Clear the current input.
564
+ removeAllChildren(this.promptDisplay);
565
+
566
+ var before, current, after;
567
+ var center = document.createElement('span');
568
+
569
+ if (this._caretPos < 0) {
570
+ before = this._input;
571
+ current = after = "";
572
+ } else if (this._caretPos === this._input.length) {
573
+ before = this._input;
574
+ current = "\u00A0";
575
+ after = "";
576
+ } else {
577
+ before = this._input.substring(0, this._caretPos);
578
+ current = this._input.charAt(this._caretPos);
579
+ after = this._input.substring(this._caretPos + 1, this._input.length);
580
+ }
581
+
582
+ this.promptDisplay.appendChild(document.createTextNode(before));
583
+ this.promptDisplay.appendChild(center);
584
+ this.promptDisplay.appendChild(document.createTextNode(after));
585
+
586
+ var hint = this.autocomplete && this.autocomplete.getSelectedWord();
587
+ addClass(center, hint ? 'console-hint' : 'console-cursor');
588
+ center.appendChild(document.createTextNode(hint ? hint.substr(this.getCurrentWord().length) : current));
589
+ };
590
+
591
+ REPLConsole.prototype.writeOutput = function(output) {
592
+ var consoleMessage = document.createElement('pre');
593
+ consoleMessage.className = "console-message";
594
+ consoleMessage.innerHTML = escapeHTML(output);
595
+ this.inner.appendChild(consoleMessage);
596
+ this.newPromptBox();
597
+ return consoleMessage;
598
+ };
599
+
600
+ REPLConsole.prototype.writeError = function(output) {
601
+ var consoleMessage = this.writeOutput(output);
602
+ addClass(consoleMessage, "error-message");
603
+ return consoleMessage;
604
+ };
605
+
606
+ REPLConsole.prototype.onEnterKey = function() {
607
+ var input = this._input;
608
+
609
+ if(input != "" && input !== undefined) {
610
+ this.commandStorage.addCommand(input);
611
+ }
612
+
613
+ this.commandHandle(input);
614
+ };
615
+
616
+ REPLConsole.prototype.onTabKey = function() {
617
+ var self = this;
618
+
619
+ var hintKeyword;
620
+ if (hintKeyword = self.getHintKeyword()) {
621
+ self.swapCurrentWord(hintKeyword);
622
+ return;
623
+ }
624
+
625
+ if (self.autocomplete) return;
626
+ self.autocomplete = new Autocomplete([]);
627
+
628
+ self.contextRequest(self.getCurrentWord(), function(err, obj) {
629
+ if (err) return self.autocomplete = false;
630
+ self.autocomplete = new Autocomplete(obj['context'], self.getCurrentWord());
631
+ self.inner.appendChild(self.autocomplete.view);
632
+ self.autocomplete.onFinished(function(word) {
633
+ self.swapCurrentWord(word);
634
+ self.autocomplete = false;
635
+ });
636
+ self.scrollToBottom();
637
+ });
638
+ };
639
+
640
+ REPLConsole.prototype.onNavigateHistory = function(offset) {
641
+ var command = this.commandStorage.navigate(offset) || "";
642
+ this.setInput(command);
643
+ };
644
+
645
+ /**
646
+ * Handle control keys like up, down, left, right.
647
+ */
648
+ REPLConsole.prototype.onKeyDown = function(ev) {
649
+ if (this.autocomplete && this.autocomplete.onKeyDown(ev)) {
650
+ this.renderInput();
651
+ ev.preventDefault();
652
+ ev.stopPropagation();
653
+ return;
654
+ }
655
+
656
+ switch (ev.keyCode) {
657
+ case 69:
658
+ // Ctrl-E
659
+ if (ev.ctrlKey) {
660
+ this.onTabKey();
661
+ ev.preventDefault();
662
+ }
663
+ break;
664
+ case 9:
665
+ // Tab
666
+ this.onTabKey();
667
+ ev.preventDefault();
668
+ break;
669
+ case 13:
670
+ // Enter key
671
+ this.onEnterKey();
672
+ ev.preventDefault();
673
+ break;
674
+ case 80:
675
+ // Ctrl-P
676
+ if (! ev.ctrlKey) break;
677
+ case 38:
678
+ // Up arrow
679
+ this.onNavigateHistory(-1);
680
+ ev.preventDefault();
681
+ break;
682
+ case 78:
683
+ // Ctrl-N
684
+ if (! ev.ctrlKey) break;
685
+ case 40:
686
+ // Down arrow
687
+ this.onNavigateHistory(1);
688
+ ev.preventDefault();
689
+ break;
690
+ case 37:
691
+ // Left arrow
692
+ var caretPos = this._caretPos > 0 ? this._caretPos - 1 : this._caretPos;
693
+ this.setInput(this._input, caretPos);
694
+ ev.preventDefault();
695
+ break;
696
+ case 39:
697
+ // Right arrow
698
+ var length = this._input.length;
699
+ var caretPos = this._caretPos < length ? this._caretPos + 1 : this._caretPos;
700
+ this.setInput(this._input, caretPos);
701
+ ev.preventDefault();
702
+ break;
703
+ case 8:
704
+ // Delete
705
+ this.deleteAtCurrent();
706
+ ev.preventDefault();
707
+ break;
708
+ default:
709
+ break;
710
+ }
711
+
712
+ if (ev.ctrlKey || ev.metaKey) {
713
+ // Set focus to our clipboard in case they hit the "v" key
714
+ this.clipboard.focus();
715
+ if (ev.keyCode == 86) {
716
+ // Pasting to clipboard doesn't happen immediately,
717
+ // so we have to wait for a while to get the pasted text.
718
+ var _this = this;
719
+ setTimeout(function() {
720
+ _this.addToInput(_this.clipboard.value);
721
+ _this.clipboard.value = "";
722
+ _this.clipboard.blur();
723
+ }, 10);
724
+ }
725
+ }
726
+
727
+ ev.stopPropagation();
728
+ };
729
+
730
+ /**
731
+ * Handle input key press.
732
+ */
733
+ REPLConsole.prototype.onKeyPress = function(ev) {
734
+ // Only write to the console if it's a single key press.
735
+ if (ev.ctrlKey || ev.metaKey) { return; }
736
+ var keyCode = ev.keyCode || ev.which;
737
+ this.insertAtCurrent(String.fromCharCode(keyCode));
738
+ ev.stopPropagation();
739
+ ev.preventDefault();
740
+ };
741
+
742
+ /**
743
+ * Delete a character at the current position.
744
+ */
745
+ REPLConsole.prototype.deleteAtCurrent = function() {
746
+ if (this._caretPos > 0) {
747
+ var caretPos = this._caretPos - 1;
748
+ var before = this._input.substring(0, caretPos);
749
+ var after = this._input.substring(this._caretPos, this._input.length);
750
+ this.setInput(before + after, caretPos);
751
+
752
+ if (!this._input) {
753
+ this.autocomplete && this.autocomplete.cancel();
754
+ this.autocomplete = false;
755
+ }
756
+ }
757
+ };
758
+
759
+ /**
760
+ * Insert a character at the current position.
761
+ */
762
+ REPLConsole.prototype.insertAtCurrent = function(char) {
763
+ var before = this._input.substring(0, this._caretPos);
764
+ var after = this._input.substring(this._caretPos, this._input.length);
765
+ this.setInput(before + char + after, this._caretPos + 1);
766
+ };
767
+
768
+ REPLConsole.prototype.swapCurrentWord = function(next) {
769
+ function right(s, pos) {
770
+ var x = s.indexOf(' ', pos);
771
+ return x === -1 ? s.length : x;
772
+ }
773
+
774
+ function swap(s, pos) {
775
+ return s.substr(0, s.lastIndexOf(' ', pos) + 1) + next + s.substr(right(s, pos))
776
+ }
777
+
778
+ if (!next) return;
779
+ var swapped = swap(this._input, this._caretPos);
780
+ this.setInput(swapped, this._caretPos + swapped.length - this._input.length);
781
+ };
782
+
783
+ REPLConsole.prototype.getCurrentWord = function() {
784
+ return (function(s, pos) {
785
+ var left = s.lastIndexOf(' ', pos);
786
+ if (left === -1) left = 0;
787
+ var right = s.indexOf(' ', pos)
788
+ if (right === -1) right = s.length - 1;
789
+ return s.substr(left, right - left + 1).replace(/^\s+|\s+$/g,'');
790
+ })(this._input, this._caretPos);
791
+ };
792
+
793
+ REPLConsole.prototype.scrollToBottom = function() {
794
+ this.outer.scrollTop = this.outer.scrollHeight;
795
+ };
796
+
797
+ // Change the binding of the console
798
+ REPLConsole.prototype.switchBindingTo = function(frameId, callback) {
799
+ var url = this.getSessionUrl('trace');
800
+ var params = "frame_id=" + encodeURIComponent(frameId);
801
+ postRequest(url, params, callback);
802
+ };
803
+
804
+ /**
805
+ * Install the console into the element with a specific ID.
806
+ * Example: REPLConsole.installInto("target-id")
807
+ */
808
+ REPLConsole.installInto = function(id, options) {
809
+ var consoleElement = document.getElementById(id);
810
+
811
+ options = options || {};
812
+
813
+ for (var prop in consoleElement.dataset) {
814
+ options[prop] = options[prop] || consoleElement.dataset[prop];
815
+ }
816
+
817
+ var replConsole = new REPLConsole(options);
818
+ replConsole.install(consoleElement);
819
+ return replConsole;
820
+ };
821
+
822
+ // This is to store the latest single session, and the stored session
823
+ // is updated by the REPLConsole#install() method.
824
+ // It allows to operate the current session from the other scripts.
825
+ REPLConsole.currentSession = null;
826
+
827
+ // This line is for the Firefox Add-on, because it doesn't have XMLHttpRequest as default.
828
+ // And so we need to require a module compatible with XMLHttpRequest from SDK.
829
+ REPLConsole.XMLHttpRequest = typeof XMLHttpRequest === 'undefined' ? null : XMLHttpRequest;
830
+
831
+ REPLConsole.request = function request(method, url, params, callback) {
832
+ var xhr = new REPLConsole.XMLHttpRequest();
833
+
834
+ xhr.open(method, url, true);
835
+ xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
836
+ xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
837
+ xhr.setRequestHeader("Accept", "<%= Mime[:web_console_v2] %>");
838
+ xhr.send(params);
839
+
840
+ xhr.onreadystatechange = function() {
841
+ if (xhr.readyState === 4) {
842
+ callback(xhr);
843
+ }
844
+ };
845
+ };
846
+
847
+ // DOM helpers
848
+ function hasClass(el, className) {
849
+ var regex = new RegExp('(?:^|\\s)' + className + '(?!\\S)', 'g');
850
+ return el.className && el.className.match(regex);
851
+ }
852
+
853
+ function isNodeList(el) {
854
+ return typeof el.length === 'number' &&
855
+ typeof el.item === 'function';
856
+ }
857
+
858
+ function addClass(el, className) {
859
+ if (isNodeList(el)) {
860
+ for (var i = 0; i < el.length; ++ i) {
861
+ addClass(el[i], className);
862
+ }
863
+ } else if (!hasClass(el, className)) {
864
+ el.className += " " + className;
865
+ }
866
+ }
867
+
868
+ function removeClass(el, className) {
869
+ var regex = new RegExp('(?:^|\\s)' + className + '(?!\\S)', 'g');
870
+ el.className = el.className.replace(regex, '');
871
+ }
872
+
873
+ function removeAllChildren(el) {
874
+ while (el.firstChild) {
875
+ el.removeChild(el.firstChild);
876
+ }
877
+ }
878
+
879
+ function findChild(el, className) {
880
+ for (var i = 0; i < el.childNodes.length; ++ i) {
881
+ if (hasClass(el.childNodes[i], className)) {
882
+ return el.childNodes[i];
883
+ }
884
+ }
885
+ }
886
+
887
+ function escapeHTML(html) {
888
+ return html
889
+ .replace(/&/g, '&amp;')
890
+ .replace(/</g, '&lt;')
891
+ .replace(/>/g, '&gt;')
892
+ .replace(/"/g, '&quot;')
893
+ .replace(/'/g, '&#x27;')
894
+ .replace(/`/g, '&#x60;');
895
+ }
896
+
897
+ // XHR helpers
898
+ function postRequest() {
899
+ REPLConsole.request.apply(this, ["POST"].concat([].slice.call(arguments)));
900
+ }
901
+
902
+ function putRequest() {
903
+ REPLConsole.request.apply(this, ["PUT"].concat([].slice.call(arguments)));
904
+ }
905
+
906
+ if (typeof exports === 'object') {
907
+ exports.REPLConsole = REPLConsole;
908
+ } else {
909
+ window.REPLConsole = REPLConsole;
910
+ }
911
+
912
+ // Split string by module operators of ruby
913
+ function getContext(s) {
914
+ var methodOp = s.lastIndexOf('.');
915
+ var moduleOp = s.lastIndexOf('::');
916
+ var x = methodOp > moduleOp ? methodOp : moduleOp;
917
+ return x !== -1 ? s.substr(0, x) : '';
918
+ }
919
+
920
+ function flatten(arrays) {
921
+ return Array.prototype.concat.apply([], arrays);
922
+ }