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.
- checksums.yaml +7 -0
- data/CHANGELOG.markdown +110 -0
- data/MIT-LICENSE +20 -0
- data/README.markdown +5 -0
- data/Rakefile +27 -0
- data/lib/web-console-compat.rb +1 -0
- data/lib/web-console.rb +1 -0
- data/lib/web_console.rb +28 -0
- data/lib/web_console/context.rb +43 -0
- data/lib/web_console/errors.rb +7 -0
- data/lib/web_console/evaluator.rb +33 -0
- data/lib/web_console/exception_mapper.rb +33 -0
- data/lib/web_console/extensions.rb +44 -0
- data/lib/web_console/integration.rb +31 -0
- data/lib/web_console/integration/cruby.rb +23 -0
- data/lib/web_console/integration/rubinius.rb +39 -0
- data/lib/web_console/locales/en.yml +15 -0
- data/lib/web_console/middleware.rb +140 -0
- data/lib/web_console/railtie.rb +71 -0
- data/lib/web_console/request.rb +50 -0
- data/lib/web_console/response.rb +23 -0
- data/lib/web_console/session.rb +76 -0
- data/lib/web_console/tasks/extensions.rake +60 -0
- data/lib/web_console/tasks/templates.rake +54 -0
- data/lib/web_console/template.rb +23 -0
- data/lib/web_console/templates/_inner_console_markup.html.erb +8 -0
- data/lib/web_console/templates/_markup.html.erb +5 -0
- data/lib/web_console/templates/_prompt_box_markup.html.erb +2 -0
- data/lib/web_console/templates/console.js.erb +922 -0
- data/lib/web_console/templates/error_page.js.erb +70 -0
- data/lib/web_console/templates/index.html.erb +8 -0
- data/lib/web_console/templates/layouts/inlined_string.erb +1 -0
- data/lib/web_console/templates/layouts/javascript.erb +5 -0
- data/lib/web_console/templates/main.js.erb +1 -0
- data/lib/web_console/templates/style.css.erb +33 -0
- data/lib/web_console/testing/erb_precompiler.rb +25 -0
- data/lib/web_console/testing/fake_middleware.rb +39 -0
- data/lib/web_console/testing/helper.rb +9 -0
- data/lib/web_console/version.rb +3 -0
- data/lib/web_console/view.rb +50 -0
- data/lib/web_console/whiny_request.rb +31 -0
- data/lib/web_console/whitelist.rb +44 -0
- 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,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, '&')
|
890
|
+
.replace(/</g, '<')
|
891
|
+
.replace(/>/g, '>')
|
892
|
+
.replace(/"/g, '"')
|
893
|
+
.replace(/'/g, ''')
|
894
|
+
.replace(/`/g, '`');
|
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
|
+
}
|