web-console 2.0.0 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of web-console might be problematic. Click here for more details.

Files changed (91) hide show
  1. checksums.yaml +4 -4
  2. data/README.markdown +132 -85
  3. data/lib/web_console.rb +14 -13
  4. data/lib/web_console/errors.rb +7 -0
  5. data/lib/web_console/{repl.rb → evaluator.rb} +7 -10
  6. data/lib/web_console/helper.rb +22 -0
  7. data/lib/web_console/integration.rb +8 -0
  8. data/lib/web_console/{core_ext/exception → integration}/cruby.rb +0 -0
  9. data/lib/web_console/integration/jruby.rb +111 -0
  10. data/lib/web_console/integration/rubinius.rb +66 -0
  11. data/lib/web_console/middleware.rb +117 -0
  12. data/lib/web_console/railtie.rb +61 -0
  13. data/lib/web_console/request.rb +30 -0
  14. data/lib/web_console/session.rb +65 -0
  15. data/lib/web_console/template.rb +49 -0
  16. data/lib/web_console/templates/_inner_console_markup.html +3 -0
  17. data/lib/web_console/templates/_markup.html +4 -0
  18. data/lib/web_console/templates/_prompt_box_markup.html +2 -0
  19. data/lib/web_console/templates/console.js +373 -0
  20. data/lib/web_console/templates/error_page.js +83 -0
  21. data/lib/web_console/templates/index.html +8 -0
  22. data/lib/web_console/templates/layouts/inlined_string.erb +1 -0
  23. data/lib/web_console/templates/layouts/javascript.erb +5 -0
  24. data/lib/web_console/templates/main.js +24 -0
  25. data/lib/web_console/templates/style.css +9 -0
  26. data/lib/web_console/version.rb +1 -1
  27. data/lib/web_console/whiny_request.rb +38 -0
  28. data/lib/web_console/whitelist.rb +42 -0
  29. data/test/dummy/config/environments/test.rb +0 -4
  30. data/test/dummy/log/development.log +7075 -0
  31. data/test/dummy/log/test.log +66006 -0
  32. data/test/dummy/tmp/cache/assets/development/sprockets/13fe41fee1fe35b49d145bcc06610705 +0 -0
  33. data/test/dummy/tmp/cache/assets/development/sprockets/2f5173deea6c795b8fdde723bb4b63af +0 -0
  34. data/test/dummy/tmp/cache/assets/development/sprockets/357970feca3ac29060c1e3861e2c0953 +0 -0
  35. data/test/dummy/tmp/cache/assets/development/sprockets/cffd775d018f68ce5dba1ee0d951a994 +0 -0
  36. data/test/dummy/tmp/cache/assets/development/sprockets/d771ace226fc8215a3572e0aa35bb0d6 +0 -0
  37. data/test/dummy/tmp/cache/assets/development/sprockets/f7cbd26ba1d28d48de824f0e94586655 +0 -0
  38. data/test/support/scenarios/bad_custom_error_scenario.rb +17 -0
  39. data/test/support/scenarios/basic_nested_scenario.rb +15 -0
  40. data/test/support/scenarios/custom_error_scenario.rb +11 -0
  41. data/test/support/scenarios/eval_nested_scenario.rb +15 -0
  42. data/test/support/scenarios/flat_scenario.rb +9 -0
  43. data/test/support/scenarios/reraised_scenario.rb +21 -0
  44. data/test/test_helper.rb +50 -3
  45. data/test/web_console/evaluator_test.rb +73 -0
  46. data/test/web_console/helper_test.rb +76 -0
  47. data/test/web_console/integration_test.rb +47 -0
  48. data/test/web_console/middleware_test.rb +116 -0
  49. data/test/web_console/railtie_test.rb +99 -0
  50. data/test/web_console/request_test.rb +52 -0
  51. data/test/web_console/session_test.rb +59 -0
  52. data/test/web_console/whiny_request_test.rb +33 -0
  53. data/test/web_console/whitelist_test.rb +43 -0
  54. metadata +66 -56
  55. data/lib/action_dispatch/debug_exceptions.rb +0 -105
  56. data/lib/action_dispatch/exception_wrapper.rb +0 -38
  57. data/lib/action_dispatch/templates/rescues/_request_and_response.html.erb +0 -34
  58. data/lib/action_dispatch/templates/rescues/_request_and_response.text.erb +0 -23
  59. data/lib/action_dispatch/templates/rescues/_source.erb +0 -29
  60. data/lib/action_dispatch/templates/rescues/_trace.html.erb +0 -72
  61. data/lib/action_dispatch/templates/rescues/_trace.text.erb +0 -9
  62. data/lib/action_dispatch/templates/rescues/_web_console.html.erb +0 -420
  63. data/lib/action_dispatch/templates/rescues/diagnostics.html.erb +0 -18
  64. data/lib/action_dispatch/templates/rescues/diagnostics.text.erb +0 -9
  65. data/lib/action_dispatch/templates/rescues/layout.erb +0 -160
  66. data/lib/action_dispatch/templates/rescues/missing_template.html.erb +0 -13
  67. data/lib/action_dispatch/templates/rescues/missing_template.text.erb +0 -3
  68. data/lib/action_dispatch/templates/rescues/routing_error.html.erb +0 -34
  69. data/lib/action_dispatch/templates/rescues/routing_error.text.erb +0 -11
  70. data/lib/action_dispatch/templates/rescues/template_error.html.erb +0 -22
  71. data/lib/action_dispatch/templates/rescues/template_error.text.erb +0 -7
  72. data/lib/action_dispatch/templates/rescues/unknown_action.html.erb +0 -6
  73. data/lib/action_dispatch/templates/rescues/unknown_action.text.erb +0 -3
  74. data/lib/action_dispatch/templates/routes/_route.html.erb +0 -16
  75. data/lib/action_dispatch/templates/routes/_table.html.erb +0 -200
  76. data/lib/assets/javascripts/web-console.js +0 -1
  77. data/lib/assets/javascripts/web_console.js +0 -41
  78. data/lib/web_console/controller_helpers.rb +0 -46
  79. data/lib/web_console/core_ext/exception.rb +0 -7
  80. data/lib/web_console/core_ext/exception/jruby.rb +0 -25
  81. data/lib/web_console/core_ext/exception/rubinius.rb +0 -32
  82. data/lib/web_console/engine.rb +0 -47
  83. data/lib/web_console/repl_session.rb +0 -89
  84. data/lib/web_console/unsupported_platforms.rb +0 -28
  85. data/lib/web_console/view_helpers.rb +0 -16
  86. data/test/action_pack/exception_wrapper_test.rb +0 -26
  87. data/test/controllers/tests_controller_test.rb +0 -41
  88. data/test/web_console/core_ext/exception_test.rb +0 -46
  89. data/test/web_console/engine_test.rb +0 -108
  90. data/test/web_console/repl_session_test.rb +0 -32
  91. data/test/web_console/repl_test.rb +0 -75
@@ -0,0 +1,66 @@
1
+ module WebConsole
2
+ module Rubinius
3
+ # Filters internal Rubinius locations.
4
+ #
5
+ # There are a couple of reasons why we wanna filter out the locations.
6
+ #
7
+ # * ::Kernel.raise, is implemented in Ruby for Rubinius. We don't wanna
8
+ # have the frame for it to align with the CRuby and JRuby implementations.
9
+ #
10
+ # * For internal methods location variables can be nil. We can't create a
11
+ # bindings for them.
12
+ #
13
+ # * Bindings from the current file are considered internal and ignored.
14
+ #
15
+ # We do that all that so we can align the bindings with the backtraces
16
+ # entries.
17
+ class InternalLocationFilter
18
+ def initialize(locations)
19
+ @locations = locations
20
+ end
21
+
22
+ def filter
23
+ @locations.reject do |location|
24
+ location.file.start_with?('kernel/delta/kernel.rb') ||
25
+ location.file == __FILE__ ||
26
+ location.variables.nil?
27
+ end
28
+ end
29
+ end
30
+
31
+ # Gets the current bindings for all available Ruby frames.
32
+ #
33
+ # Filters the internal Rubinius and WebConsole frames.
34
+ def self.current_bindings
35
+ locations = ::Rubinius::VM.backtrace(1, true)
36
+
37
+ InternalLocationFilter.new(locations).filter.map do |location|
38
+ Binding.setup(
39
+ location.variables,
40
+ location.variables.method,
41
+ location.constant_scope,
42
+ location.variables.self,
43
+ location
44
+ )
45
+ end
46
+ end
47
+ end
48
+ end
49
+
50
+ ::Exception.class_eval do
51
+ def bindings
52
+ @bindings || []
53
+ end
54
+ end
55
+
56
+ ::Rubinius.singleton_class.class_eval do
57
+ def raise_exception_with_current_bindings(exc)
58
+ if exc.bindings.empty?
59
+ exc.instance_variable_set(:@bindings, WebConsole::Rubinius.current_bindings)
60
+ end
61
+
62
+ raise_exception_without_current_bindings(exc)
63
+ end
64
+
65
+ alias_method_chain :raise_exception, :current_bindings
66
+ end
@@ -0,0 +1,117 @@
1
+ require 'active_support/core_ext/string/strip'
2
+
3
+ module WebConsole
4
+ class Middleware
5
+ TEMPLATES_PATH = File.expand_path('../templates', __FILE__)
6
+
7
+ DEFAULT_OPTIONS = {
8
+ update_re: %r{/repl_sessions/(?<id>.+?)\z},
9
+ binding_change_re: %r{/repl_sessions/(?<id>.+?)/trace\z}
10
+ }
11
+
12
+ UNAVAILABLE_SESSION_MESSAGE = <<-END.strip_heredoc
13
+ Session %{id} is is no longer available in memory.
14
+
15
+ If you happen to run on a multi-process server (like Unicorn) the process
16
+ this request hit doesn't store %{id} in memory.
17
+ END
18
+
19
+ cattr_accessor :whiny_requests
20
+ @@whiny_requests = true
21
+
22
+ def initialize(app, options = {})
23
+ @app = app
24
+ @options = DEFAULT_OPTIONS.merge(options)
25
+ end
26
+
27
+ def call(env)
28
+ request = create_regular_or_whiny_request(env)
29
+ return @app.call(env) unless request.from_whitelited_ip?
30
+
31
+ if id = id_for_repl_session_update(request)
32
+ return update_repl_session(id, request.params)
33
+ elsif id = id_for_repl_session_stack_frame_change(request)
34
+ return change_stack_trace(id, request.params)
35
+ end
36
+
37
+ status, headers, body = @app.call(env)
38
+
39
+ if exception = env['web_console.exception']
40
+ session = Session.from_exception(exception)
41
+ elsif binding = env['web_console.binding']
42
+ session = Session.from_binding(binding)
43
+ end
44
+
45
+ if session && request.acceptable_content_type?
46
+ response = Rack::Response.new(body, status, headers)
47
+ template = Template.new(env, session)
48
+
49
+ response.write(template.render('index'))
50
+ response.finish
51
+ else
52
+ [ status, headers, body ]
53
+ end
54
+ end
55
+
56
+ private
57
+
58
+ def create_regular_or_whiny_request(env)
59
+ request = Request.new(env)
60
+ whiny_requests ? WhinyRequest.new(request) : request
61
+ end
62
+
63
+ def update_re
64
+ @options[:update_re]
65
+ end
66
+
67
+ def binding_change_re
68
+ @options[:binding_change_re]
69
+ end
70
+
71
+ def id_for_repl_session_update(request)
72
+ if request.xhr? && request.put?
73
+ update_re.match(request.path_info) { |m| m[:id] }
74
+ end
75
+ end
76
+
77
+ def id_for_repl_session_stack_frame_change(request)
78
+ if request.xhr? && request.post?
79
+ binding_change_re.match(request.path_info) { |m| m[:id] }
80
+ end
81
+ end
82
+
83
+ def update_repl_session(id, params)
84
+ unless session = Session.find(id)
85
+ return respond_with_unavailable_session(id)
86
+ end
87
+
88
+ status = 200
89
+ headers = { 'Content-Type' => 'application/json; charset = utf-8' }
90
+ body = { output: session.eval(params[:input]) }.to_json
91
+
92
+ Rack::Response.new(body, status, headers).finish
93
+ end
94
+
95
+ def change_stack_trace(id, params)
96
+ unless session = Session.find(id)
97
+ return respond_with_unavailable_session(id)
98
+ end
99
+
100
+ session.switch_binding_to(params[:frame_id])
101
+
102
+ status = 200
103
+ headers = { 'Content-Type' => 'application/json; charset = utf-8' }
104
+ body = { ok: true }.to_json
105
+
106
+ Rack::Response.new(body, status, headers).finish
107
+ end
108
+
109
+ def respond_with_unavailable_session(id)
110
+ status = 404
111
+ headers = { 'Content-Type' => 'application/json; charset = utf-8' }
112
+ body = { output: format(UNAVAILABLE_SESSION_MESSAGE, id: id)}.to_json
113
+
114
+ Rack::Response.new(body, status, headers).finish
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,61 @@
1
+ require 'rails/railtie'
2
+
3
+ module WebConsole
4
+ class Railtie < ::Rails::Railtie
5
+ config.web_console = ActiveSupport::OrderedOptions.new
6
+ config.web_console.whitelisted_ips = %w( 127.0.0.1 ::1 )
7
+
8
+ initializer 'web_console.initialize' do
9
+ ActionDispatch::DebugExceptions.class_eval do
10
+ def render_exception_with_web_console(env, exception)
11
+ render_exception_without_web_console(env, exception).tap do
12
+ wrapper = ActionDispatch::ExceptionWrapper.new(env, exception)
13
+
14
+ # Get the original exception if ExceptionWrapper decides to follow it.
15
+ env['web_console.exception'] = wrapper.exception
16
+ end
17
+ end
18
+
19
+ alias_method_chain :render_exception, :web_console
20
+ end
21
+
22
+ ActiveSupport.on_load(:action_view) do
23
+ ActionView::Base.send(:include, Helper)
24
+ end
25
+
26
+ ActiveSupport.on_load(:action_controller) do
27
+ ActionController::Base.send(:include, Helper)
28
+ end
29
+ end
30
+
31
+ initializer 'web_console.insert_middleware' do |app|
32
+ app.middleware.insert_before ActionDispatch::DebugExceptions, Middleware
33
+ end
34
+
35
+ initializer 'web_console.templates_path' do
36
+ if template_paths = config.web_console.template_paths
37
+ Template.template_paths.unshift(*Array(template_paths))
38
+ end
39
+ end
40
+
41
+ initializer 'web_console.whitelisted_ips' do
42
+ if whitelisted_ips = config.web_console.whitelisted_ips
43
+ Request.whitelisted_ips = Whitelist.new(whitelisted_ips)
44
+ end
45
+ end
46
+
47
+ initializer 'web_console.whiny_requests' do
48
+ if config.web_console.key?(:whiny_requests)
49
+ Middleware.whiny_requests = config.web_console.whiny_requests
50
+ end
51
+ end
52
+
53
+ # Leave this undocumented so we treat such content type misses as bugs,
54
+ # while still being able to help the affected users in the meantime.
55
+ initializer 'web_console.acceptable_content_types' do
56
+ if acceptable_content_types = config.web_console.acceptable_content_types
57
+ Request.acceptable_content_types.concat(Array(acceptable_content_types))
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,30 @@
1
+ module WebConsole
2
+ # Web Console tailored request object.
3
+ class Request < ActionDispatch::Request
4
+ # While most of the servers will return blank content type if none given,
5
+ # Puma will return text/plain.
6
+ cattr_accessor :acceptable_content_types
7
+ @@acceptable_content_types = [Mime::HTML, Mime::TEXT]
8
+
9
+ # Configurable set of whitelisted networks.
10
+ cattr_accessor :whitelisted_ips
11
+ @@whitelisted_ips = Whitelist.new
12
+
13
+ # Returns whether a request came from a whitelisted IP.
14
+ #
15
+ # For a request to hit Web Console features, it needs to come from a white
16
+ # listed IP.
17
+ def from_whitelited_ip?
18
+ whitelisted_ips.include?(remote_ip)
19
+ end
20
+
21
+ # Returns whether the request is from an acceptable content type.
22
+ #
23
+ # We can render a console for HTML and TEXT by default. If a client didn't
24
+ # specified any content type and the server returned it as blank, we'll
25
+ # render it as well.
26
+ def acceptable_content_type?
27
+ content_type.blank? || content_type.in?(acceptable_content_types)
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,65 @@
1
+ module WebConsole
2
+ # A session lets you persist wrap an +Evaluator+ instance in memory
3
+ # associated with multiple bindings.
4
+ #
5
+ # Each newly created session is persisted into memory and you can find it
6
+ # later its +id+.
7
+ #
8
+ # A session may be associated with multiple bindings. This is used by the
9
+ # error pages only, as currently, this is the only client that needs to do
10
+ # that.
11
+ class Session
12
+ INMEMORY_STORAGE = {}
13
+
14
+ class << self
15
+ # Finds a persisted session in memory by its id.
16
+ #
17
+ # Returns a persisted session if found in memory.
18
+ # Raises NotFound error unless found in memory.
19
+ def find(id)
20
+ INMEMORY_STORAGE[id]
21
+ end
22
+
23
+ # Create a Session from an exception.
24
+ def from_exception(exc)
25
+ new(exc.bindings)
26
+ end
27
+
28
+ # Create a Session from a single binding.
29
+ def from_binding(binding)
30
+ new(binding)
31
+ end
32
+ end
33
+
34
+ # An unique identifier for every REPL.
35
+ attr_reader :id
36
+
37
+ def initialize(bindings)
38
+ @id = SecureRandom.hex(16)
39
+ @bindings = Array(bindings)
40
+ @evaluator = Evaluator.new(@bindings[0])
41
+
42
+ store_into_memory
43
+ end
44
+
45
+ # Evaluate +input+ on the current Evaluator associated binding.
46
+ #
47
+ # Returns a string of the Evaluator output.
48
+ def eval(input)
49
+ @evaluator.eval(input)
50
+ end
51
+
52
+ # Switches the current binding to the one at specified +index+.
53
+ #
54
+ # Returns nothing.
55
+ def switch_binding_to(index)
56
+ @evaluator = Evaluator.new(@bindings[index.to_i])
57
+ end
58
+
59
+ private
60
+
61
+ def store_into_memory
62
+ INMEMORY_STORAGE[id] = self
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,49 @@
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
+ class Context < ActionView::Base
8
+ # Execute a block only on error pages.
9
+ #
10
+ # The error pages are special, because they are the only pages that
11
+ # currently require multiple bindings. We get those from exceptions.
12
+ def only_on_error_page(*args)
13
+ yield if @env['web_console.exception'].present?
14
+ end
15
+
16
+ # Render JavaScript inside a script tag and a closure.
17
+ #
18
+ # This one lets write JavaScript that will automatically get wrapped in a
19
+ # script tag and enclosed in a closure, so you don't have to worry for
20
+ # leaking globals, unless you explicitly want to.
21
+ def render_javascript(template)
22
+ render(template: template, layout: 'layouts/javascript')
23
+ end
24
+
25
+ # Render inlined string to be used inside of JavaScript code.
26
+ #
27
+ # The inlined string is returned as an actual JavaScript string. You
28
+ # don't need to wrap the result yourself.
29
+ def render_inlined_string(template)
30
+ render(template: template, layout: 'layouts/inlined_string')
31
+ end
32
+ end
33
+
34
+ # Lets you customize the default templates folder location.
35
+ cattr_accessor :template_paths
36
+ @@template_paths = [ File.expand_path('../templates', __FILE__) ]
37
+
38
+ def initialize(env, session)
39
+ @env = env
40
+ @session = session
41
+ end
42
+
43
+ # Render a template (inferred from +template_paths+) as a plain string.
44
+ def render(template)
45
+ context = Context.new(template_paths, instance_values)
46
+ context.render(template: template, layout: false)
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,3 @@
1
+ <div id='resizer'></div>
2
+ <div class='console-inner'></div>
3
+ <input id='clipboard' type='text'>
@@ -0,0 +1,4 @@
1
+ <div id="console"
2
+ data-remote-path='<%= "console/repl_sessions/#{@session.id}" %>'
3
+ data-initial-prompt='>> '>
4
+ </div>
@@ -0,0 +1,2 @@
1
+ <span class='console-prompt-label'></span>
2
+ <pre class='console-prompt-display'></pre>
@@ -0,0 +1,373 @@
1
+ // DOM helpers
2
+ function hasClass(el, className) {
3
+ var regex = new RegExp('(?:^|\\s)' + className + '(?!\\S)', 'g');
4
+ return el.className.match(regex);
5
+ }
6
+
7
+ function addClass(el, className) {
8
+ el.className += " " + className;
9
+ }
10
+
11
+ function removeClass(el, className) {
12
+ var regex = new RegExp('(?:^|\\s)' + className + '(?!\\S)', 'g');
13
+ el.className = el.className.replace(regex, '');
14
+ }
15
+
16
+ function removeAllChildren(el) {
17
+ while (el.firstChild) {
18
+ el.removeChild(el.firstChild);
19
+ }
20
+ }
21
+
22
+ function escapeHTML(html) {
23
+ return html
24
+ .replace(/&/g, '&amp;')
25
+ .replace(/</g, '&lt;')
26
+ .replace(/>/g, '&gt;')
27
+ .replace(/"/g, '&quot;')
28
+ .replace(/'/g, '&#x27;')
29
+ .replace(/`/g, '&#x60;');
30
+ }
31
+
32
+ // Add CSS styles dynamically. This probably doesnt work for IE <8.
33
+ var style = document.createElement('style');
34
+ style.type = 'text/css';
35
+ style.innerHTML = <%= render_inlined_string 'style.css' %>;
36
+ document.getElementsByTagName('head')[0].appendChild(style);
37
+
38
+ /**
39
+ * Constructor for command storage.
40
+ * It uses localStorage if available. Otherwise fallback to normal JS array.
41
+ */
42
+ function CommandStorage() {
43
+ this.previousCommands = [];
44
+ var previousCommandOffset = 0;
45
+ var hasLocalStorage = typeof window.localStorage !== 'undefined';
46
+ var STORAGE_KEY = "web_console_previous_commands";
47
+ var MAX_STORAGE = 100;
48
+
49
+ if (hasLocalStorage) {
50
+ this.previousCommands = JSON.parse(localStorage.getItem(STORAGE_KEY)) || [];
51
+ previousCommandOffset = this.previousCommands.length;
52
+ }
53
+
54
+ this.addCommand = function(command) {
55
+ previousCommandOffset = this.previousCommands.push(command);
56
+
57
+ if (previousCommandOffset > MAX_STORAGE) {
58
+ this.previousCommands.splice(0, 1);
59
+ previousCommandOffset = MAX_STORAGE;
60
+ }
61
+
62
+ if (hasLocalStorage) {
63
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(this.previousCommands));
64
+ }
65
+ };
66
+
67
+ this.navigate = function(offset) {
68
+ previousCommandOffset += offset;
69
+
70
+ if (previousCommandOffset < 0) {
71
+ previousCommandOffset = -1;
72
+ return null;
73
+ }
74
+
75
+ if (previousCommandOffset >= this.previousCommands.length) {
76
+ previousCommandOffset = this.previousCommands.length;
77
+ return null;
78
+ }
79
+
80
+ return this.previousCommands[previousCommandOffset];
81
+ }
82
+ }
83
+
84
+ // HTML strings for dynamic elements.
85
+ var consoleInnerHtml = <%= render_inlined_string '_inner_console_markup.html' %>;
86
+ var promptBoxHtml = <%= render_inlined_string '_prompt_box_markup.html' %>;
87
+
88
+ // REPLConsole Constructor
89
+ function REPLConsole(config) {
90
+ this.commandStorage = new CommandStorage();
91
+ this.prompt = config && config.promptLabel ? config.promptLabel : ' >>';
92
+ this.commandHandle = config && config.commandHandle ? config.commandHandle : function() { return this; }
93
+ }
94
+
95
+
96
+ REPLConsole.prototype.install = function(container) {
97
+ var _this = this;
98
+
99
+ document.onkeydown = function(ev) {
100
+ if (_this.focused) { _this.onKeyDown(ev); }
101
+ };
102
+
103
+ document.onkeypress = function(ev) {
104
+ if (_this.focused) { _this.onKeyPress(ev); }
105
+ };
106
+
107
+ document.addEventListener('mousedown', function(ev) {
108
+ var el = ev.target || ev.srcElement;
109
+
110
+ if (el) {
111
+ do {
112
+ if (el === container) {
113
+ _this.focus();
114
+ return;
115
+ }
116
+ } while (el = el.parentNode);
117
+
118
+ _this.blur();
119
+ }
120
+ });
121
+
122
+ // Render the console.
123
+ container.innerHTML = consoleInnerHtml;
124
+
125
+ // Make the console resizable.
126
+ document.getElementById('resizer').addEventListener('mousedown', function(ev) {
127
+ var startY = ev.clientY;
128
+ var startHeight = parseInt(document.defaultView.getComputedStyle(container).height, 10);
129
+ var consoleInner = document.getElementsByClassName('console-inner')[0];
130
+ var innerScrollTopStart = consoleInner.scrollTop;
131
+ var innerClientHeightStart = consoleInner.clientHeight;
132
+
133
+ var doDrag = function(e) {
134
+ container.style.height = (startHeight + startY - e.clientY) + 'px';
135
+ consoleInner.scrollTop = innerScrollTopStart + (innerClientHeightStart - consoleInner.clientHeight);
136
+ };
137
+
138
+ var stopDrag = function(e) {
139
+ document.documentElement.removeEventListener('mousemove', doDrag, false);
140
+ document.documentElement.removeEventListener('mouseup', stopDrag, false);
141
+ };
142
+
143
+ document.documentElement.addEventListener('mousemove', doDrag, false);
144
+ document.documentElement.addEventListener('mouseup', stopDrag, false);
145
+ });
146
+
147
+ // Initialize
148
+ this.inner = container.getElementsByClassName('console-inner')[0];
149
+ this.clipboard = document.getElementById('clipboard');
150
+ this.newPromptBox();
151
+ };
152
+
153
+ REPLConsole.prototype.focus = function() {
154
+ if (! this.focused) {
155
+ this.focused = true;
156
+ if (! hasClass(this.inner, "console-focus")) {
157
+ addClass(this.inner, "console-focus");
158
+ }
159
+ this.scrollToBottom();
160
+ }
161
+ };
162
+
163
+ REPLConsole.prototype.blur = function() {
164
+ this.focused = false;
165
+ removeClass(this.inner, "console-focus");
166
+ };
167
+
168
+ /**
169
+ * Add a new empty prompt box to the console.
170
+ */
171
+ REPLConsole.prototype.newPromptBox = function() {
172
+ // Remove the caret from previous prompt display if any.
173
+ if (this.promptDisplay) {
174
+ this.removeCaretFromPrompt();
175
+ }
176
+
177
+ var promptBox = document.createElement('div');
178
+ promptBox.className = "console-prompt-box";
179
+ promptBox.innerHTML = promptBoxHtml;
180
+ this.promptLabel = promptBox.getElementsByClassName('console-prompt-label')[0];
181
+ this.promptDisplay = promptBox.getElementsByClassName('console-prompt-display')[0];
182
+ // Render the prompt box
183
+ this.setInput("");
184
+ this.promptLabel.innerHTML = this.prompt;
185
+ this.inner.appendChild(promptBox);
186
+ this.scrollToBottom();
187
+ };
188
+
189
+ /**
190
+ * Remove the caret from the prompt box,
191
+ * mainly before adding a new prompt box.
192
+ * For simplicity, just re-render the prompt box
193
+ * with caret position -1.
194
+ */
195
+ REPLConsole.prototype.removeCaretFromPrompt = function() {
196
+ this.setInput(this._input, -1);
197
+ };
198
+
199
+ REPLConsole.prototype.setInput = function(input, caretPos) {
200
+ this._caretPos = caretPos === undefined ? input.length : caretPos;
201
+ this._input = input;
202
+ this.renderInput();
203
+ };
204
+
205
+ /**
206
+ * Add some text to the existing input.
207
+ */
208
+ REPLConsole.prototype.addToInput = function(val, caretPos) {
209
+ caretPos = caretPos || this._caretPos;
210
+ var before = this._input.substring(0, caretPos);
211
+ var after = this._input.substring(caretPos, this._input.length);
212
+ var newInput = before + val + after;
213
+ this.setInput(newInput, caretPos + val.length);
214
+ };
215
+
216
+ /**
217
+ * Render the input prompt. This is called whenever
218
+ * the user input changes, sometimes not very efficient.
219
+ */
220
+ REPLConsole.prototype.renderInput = function() {
221
+ // Clear the current input.
222
+ removeAllChildren(this.promptDisplay);
223
+
224
+ var promptCursor = document.createElement('span');
225
+ promptCursor.className = "console-cursor";
226
+ var before, current, after;
227
+
228
+ if (this._caretPos < 0) {
229
+ before = this._input;
230
+ current = after = "";
231
+ } else if (this._caretPos === this._input.length) {
232
+ before = this._input;
233
+ current = "\u00A0";
234
+ after = "";
235
+ } else {
236
+ before = this._input.substring(0, this._caretPos);
237
+ current = this._input.charAt(this._caretPos);
238
+ after = this._input.substring(this._caretPos + 1, this._input.length);
239
+ }
240
+
241
+ this.promptDisplay.appendChild(document.createTextNode(before));
242
+ promptCursor.appendChild(document.createTextNode(current));
243
+ this.promptDisplay.appendChild(promptCursor);
244
+ this.promptDisplay.appendChild(document.createTextNode(after));
245
+ };
246
+
247
+ REPLConsole.prototype.writeOutput = function(output) {
248
+ var consoleMessage = document.createElement('pre');
249
+ consoleMessage.className = "console-message";
250
+ consoleMessage.innerHTML = escapeHTML(output);
251
+ this.inner.appendChild(consoleMessage);
252
+ this.newPromptBox();
253
+ };
254
+
255
+ REPLConsole.prototype.onEnterKey = function() {
256
+ var input = this._input;
257
+
258
+ if(input != "" && input !== undefined) {
259
+ this.commandStorage.addCommand(input);
260
+ }
261
+
262
+ this.commandHandle(input);
263
+ };
264
+
265
+ REPLConsole.prototype.onNavigateHistory = function(offset) {
266
+ var command = this.commandStorage.navigate(offset) || "";
267
+ this.setInput(command);
268
+ };
269
+
270
+ /**
271
+ * Handle control keys like up, down, left, right.
272
+ */
273
+ REPLConsole.prototype.onKeyDown = function(ev) {
274
+ switch (ev.keyCode) {
275
+ case 13:
276
+ // Enter key
277
+ this.onEnterKey();
278
+ ev.preventDefault();
279
+ break;
280
+ case 80:
281
+ // Ctrl-P
282
+ if (! ev.ctrlKey) break;
283
+ case 38:
284
+ // Up arrow
285
+ this.onNavigateHistory(-1);
286
+ ev.preventDefault();
287
+ break;
288
+ case 78:
289
+ // Ctrl-N
290
+ if (! ev.ctrlKey) break;
291
+ case 40:
292
+ // Down arrow
293
+ this.onNavigateHistory(1);
294
+ ev.preventDefault();
295
+ break;
296
+ case 37:
297
+ // Left arrow
298
+ var caretPos = this._caretPos > 0 ? this._caretPos - 1 : this._caretPos;
299
+ this.setInput(this._input, caretPos);
300
+ ev.preventDefault();
301
+ break;
302
+ case 39:
303
+ // Right arrow
304
+ var length = this._input.length;
305
+ var caretPos = this._caretPos < length ? this._caretPos + 1 : this._caretPos;
306
+ this.setInput(this._input, caretPos);
307
+ ev.preventDefault();
308
+ break;
309
+ case 8:
310
+ // Delete
311
+ this.deleteAtCurrent();
312
+ ev.preventDefault();
313
+ break;
314
+ default:
315
+ break;
316
+ }
317
+
318
+ if (ev.ctrlKey || ev.metaKey) {
319
+ // Set focus to our clipboard in case they hit the "v" key
320
+ this.clipboard.focus();
321
+ if (ev.keyCode == 86) {
322
+ // Pasting to clipboard doesn't happen immediately,
323
+ // so we have to wait for a while to get the pasted text.
324
+ var _this = this;
325
+ setTimeout(function() {
326
+ _this.addToInput(_this.clipboard.value);
327
+ _this.clipboard.value = "";
328
+ _this.clipboard.blur();
329
+ }, 10);
330
+ }
331
+ }
332
+
333
+ ev.stopPropagation();
334
+ };
335
+
336
+ /**
337
+ * Handle input key press.
338
+ */
339
+ REPLConsole.prototype.onKeyPress = function(ev) {
340
+ // Only write to the console if it's a single key press.
341
+ if (ev.ctrlKey || ev.metaKey) { return; }
342
+ var keyCode = ev.keyCode || ev.which;
343
+ this.insertAtCurrent(String.fromCharCode(keyCode));
344
+ ev.stopPropagation();
345
+ ev.preventDefault();
346
+ };
347
+
348
+ /**
349
+ * Delete a character at the current position.
350
+ */
351
+ REPLConsole.prototype.deleteAtCurrent = function() {
352
+ if (this._caretPos > 0) {
353
+ var caretPos = this._caretPos - 1;
354
+ var before = this._input.substring(0, caretPos);
355
+ var after = this._input.substring(this._caretPos, this._input.length);
356
+ this.setInput(before + after, caretPos);
357
+ }
358
+ };
359
+
360
+ /**
361
+ * Insert a character at the current position.
362
+ */
363
+ REPLConsole.prototype.insertAtCurrent = function(char) {
364
+ var before = this._input.substring(0, this._caretPos);
365
+ var after = this._input.substring(this._caretPos, this._input.length);
366
+ this.setInput(before + char + after, this._caretPos + 1);
367
+ };
368
+
369
+ REPLConsole.prototype.scrollToBottom = function() {
370
+ this.inner.scrollTop = this.inner.scrollHeight;
371
+ };
372
+
373
+ window.REPLConsole = REPLConsole;