web-console-compat 3.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
+ }