web-console 2.0.0 → 2.1.0

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 (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;