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