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.
- checksums.yaml +4 -4
- data/README.markdown +132 -85
- data/lib/web_console.rb +14 -13
- data/lib/web_console/errors.rb +7 -0
- data/lib/web_console/{repl.rb → evaluator.rb} +7 -10
- data/lib/web_console/helper.rb +22 -0
- data/lib/web_console/integration.rb +8 -0
- data/lib/web_console/{core_ext/exception → integration}/cruby.rb +0 -0
- data/lib/web_console/integration/jruby.rb +111 -0
- data/lib/web_console/integration/rubinius.rb +66 -0
- data/lib/web_console/middleware.rb +117 -0
- data/lib/web_console/railtie.rb +61 -0
- data/lib/web_console/request.rb +30 -0
- data/lib/web_console/session.rb +65 -0
- data/lib/web_console/template.rb +49 -0
- data/lib/web_console/templates/_inner_console_markup.html +3 -0
- data/lib/web_console/templates/_markup.html +4 -0
- data/lib/web_console/templates/_prompt_box_markup.html +2 -0
- data/lib/web_console/templates/console.js +373 -0
- data/lib/web_console/templates/error_page.js +83 -0
- data/lib/web_console/templates/index.html +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 +24 -0
- data/lib/web_console/templates/style.css +9 -0
- data/lib/web_console/version.rb +1 -1
- data/lib/web_console/whiny_request.rb +38 -0
- data/lib/web_console/whitelist.rb +42 -0
- data/test/dummy/config/environments/test.rb +0 -4
- data/test/dummy/log/development.log +7075 -0
- data/test/dummy/log/test.log +66006 -0
- data/test/dummy/tmp/cache/assets/development/sprockets/13fe41fee1fe35b49d145bcc06610705 +0 -0
- data/test/dummy/tmp/cache/assets/development/sprockets/2f5173deea6c795b8fdde723bb4b63af +0 -0
- data/test/dummy/tmp/cache/assets/development/sprockets/357970feca3ac29060c1e3861e2c0953 +0 -0
- data/test/dummy/tmp/cache/assets/development/sprockets/cffd775d018f68ce5dba1ee0d951a994 +0 -0
- data/test/dummy/tmp/cache/assets/development/sprockets/d771ace226fc8215a3572e0aa35bb0d6 +0 -0
- data/test/dummy/tmp/cache/assets/development/sprockets/f7cbd26ba1d28d48de824f0e94586655 +0 -0
- data/test/support/scenarios/bad_custom_error_scenario.rb +17 -0
- data/test/support/scenarios/basic_nested_scenario.rb +15 -0
- data/test/support/scenarios/custom_error_scenario.rb +11 -0
- data/test/support/scenarios/eval_nested_scenario.rb +15 -0
- data/test/support/scenarios/flat_scenario.rb +9 -0
- data/test/support/scenarios/reraised_scenario.rb +21 -0
- data/test/test_helper.rb +50 -3
- data/test/web_console/evaluator_test.rb +73 -0
- data/test/web_console/helper_test.rb +76 -0
- data/test/web_console/integration_test.rb +47 -0
- data/test/web_console/middleware_test.rb +116 -0
- data/test/web_console/railtie_test.rb +99 -0
- data/test/web_console/request_test.rb +52 -0
- data/test/web_console/session_test.rb +59 -0
- data/test/web_console/whiny_request_test.rb +33 -0
- data/test/web_console/whitelist_test.rb +43 -0
- metadata +66 -56
- data/lib/action_dispatch/debug_exceptions.rb +0 -105
- data/lib/action_dispatch/exception_wrapper.rb +0 -38
- data/lib/action_dispatch/templates/rescues/_request_and_response.html.erb +0 -34
- data/lib/action_dispatch/templates/rescues/_request_and_response.text.erb +0 -23
- data/lib/action_dispatch/templates/rescues/_source.erb +0 -29
- data/lib/action_dispatch/templates/rescues/_trace.html.erb +0 -72
- data/lib/action_dispatch/templates/rescues/_trace.text.erb +0 -9
- data/lib/action_dispatch/templates/rescues/_web_console.html.erb +0 -420
- data/lib/action_dispatch/templates/rescues/diagnostics.html.erb +0 -18
- data/lib/action_dispatch/templates/rescues/diagnostics.text.erb +0 -9
- data/lib/action_dispatch/templates/rescues/layout.erb +0 -160
- data/lib/action_dispatch/templates/rescues/missing_template.html.erb +0 -13
- data/lib/action_dispatch/templates/rescues/missing_template.text.erb +0 -3
- data/lib/action_dispatch/templates/rescues/routing_error.html.erb +0 -34
- data/lib/action_dispatch/templates/rescues/routing_error.text.erb +0 -11
- data/lib/action_dispatch/templates/rescues/template_error.html.erb +0 -22
- data/lib/action_dispatch/templates/rescues/template_error.text.erb +0 -7
- data/lib/action_dispatch/templates/rescues/unknown_action.html.erb +0 -6
- data/lib/action_dispatch/templates/rescues/unknown_action.text.erb +0 -3
- data/lib/action_dispatch/templates/routes/_route.html.erb +0 -16
- data/lib/action_dispatch/templates/routes/_table.html.erb +0 -200
- data/lib/assets/javascripts/web-console.js +0 -1
- data/lib/assets/javascripts/web_console.js +0 -41
- data/lib/web_console/controller_helpers.rb +0 -46
- data/lib/web_console/core_ext/exception.rb +0 -7
- data/lib/web_console/core_ext/exception/jruby.rb +0 -25
- data/lib/web_console/core_ext/exception/rubinius.rb +0 -32
- data/lib/web_console/engine.rb +0 -47
- data/lib/web_console/repl_session.rb +0 -89
- data/lib/web_console/unsupported_platforms.rb +0 -28
- data/lib/web_console/view_helpers.rb +0 -16
- data/test/action_pack/exception_wrapper_test.rb +0 -26
- data/test/controllers/tests_controller_test.rb +0 -41
- data/test/web_console/core_ext/exception_test.rb +0 -46
- data/test/web_console/engine_test.rb +0 -108
- data/test/web_console/repl_session_test.rb +0 -32
- 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,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, '&')
|
25
|
+
.replace(/</g, '<')
|
26
|
+
.replace(/>/g, '>')
|
27
|
+
.replace(/"/g, '"')
|
28
|
+
.replace(/'/g, ''')
|
29
|
+
.replace(/`/g, '`');
|
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;
|