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
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
module ActionDispatch
|
|
2
|
-
class DebugExceptions
|
|
3
|
-
RESCUES_TEMPLATE_PATH.replace(File.expand_path('../templates', __FILE__))
|
|
4
|
-
|
|
5
|
-
def call(env)
|
|
6
|
-
request = Request.new(env)
|
|
7
|
-
|
|
8
|
-
if request.put? && request.xhr? && allowed?(request) && m = env["PATH_INFO"].match(%r{/repl_sessions/(?<id>.+?)\z})
|
|
9
|
-
update_repl_session(m[:id], request.params[:input])
|
|
10
|
-
elsif request.post? && request.xhr? && allowed?(request) && m = env["PATH_INFO"].match(%r{/repl_sessions/(?<id>.+?)/trace\z})
|
|
11
|
-
change_stack_trace(m[:id], request.params[:frame_id])
|
|
12
|
-
else
|
|
13
|
-
middleware_call(env)
|
|
14
|
-
end
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
def middleware_call(env)
|
|
18
|
-
_, headers, body = response = @app.call(env)
|
|
19
|
-
|
|
20
|
-
if headers['X-Cascade'] == 'pass'
|
|
21
|
-
body.close if body.respond_to?(:close)
|
|
22
|
-
raise ActionController::RoutingError, "No route matches [#{env['REQUEST_METHOD']}] #{env['PATH_INFO'].inspect}"
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
response
|
|
26
|
-
rescue Exception => exception
|
|
27
|
-
raise exception if env['action_dispatch.show_exceptions'] == false
|
|
28
|
-
render_exception(env, exception)
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
private
|
|
32
|
-
|
|
33
|
-
def allowed?(request)
|
|
34
|
-
request.remote_ip.in?(WebConsole.config.whitelisted_ips)
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
def update_repl_session(id, input)
|
|
38
|
-
console_session = WebConsole::REPLSession.find(id)
|
|
39
|
-
response = console_session.save(input: input)
|
|
40
|
-
[ 200, { "Content-Type" => "text/plain; charset=utf-8" }, [ response.to_json ] ]
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
def change_stack_trace(id, frame_id)
|
|
44
|
-
console_session = WebConsole::REPLSession.find(id)
|
|
45
|
-
binding = console_session.binding_stack[frame_id.to_i]
|
|
46
|
-
console_session.binding = binding
|
|
47
|
-
[ 200, { "Content-Type" => "text/plain; charset=utf-8" }, [ JSON.dump("success") ] ]
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
def render_exception(env, exception)
|
|
51
|
-
wrapper = ExceptionWrapper.new(env, exception)
|
|
52
|
-
log_error(env, wrapper)
|
|
53
|
-
|
|
54
|
-
if env['action_dispatch.show_detailed_exceptions']
|
|
55
|
-
request = Request.new(env)
|
|
56
|
-
if allowed?(request)
|
|
57
|
-
console_session = WebConsole::REPLSession.create(
|
|
58
|
-
binding: wrapper.exception.bindings.first,
|
|
59
|
-
binding_stack: exception.bindings
|
|
60
|
-
)
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
traces = wrapper.traces
|
|
64
|
-
extract_sources = wrapper.extract_sources
|
|
65
|
-
console_session = WebConsole::REPLSession.create(
|
|
66
|
-
binding: exception.bindings.first,
|
|
67
|
-
binding_stack: exception.bindings
|
|
68
|
-
)
|
|
69
|
-
|
|
70
|
-
trace_to_show = 'Application Trace'
|
|
71
|
-
if traces[trace_to_show].empty? && wrapper.rescue_template != 'routing_error'
|
|
72
|
-
trace_to_show = 'Full Trace'
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
if source_to_show = traces[trace_to_show].first
|
|
76
|
-
source_to_show_id = source_to_show[:id]
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
template = ActionView::Base.new([ RESCUES_TEMPLATE_PATH ],
|
|
80
|
-
request: request,
|
|
81
|
-
exception: wrapper.exception,
|
|
82
|
-
show_source_idx: source_to_show_id,
|
|
83
|
-
trace_to_show: trace_to_show,
|
|
84
|
-
traces: traces,
|
|
85
|
-
routes_inspector: routes_inspector(exception),
|
|
86
|
-
source_extract: wrapper.extract_sources,
|
|
87
|
-
console_session: console_session
|
|
88
|
-
)
|
|
89
|
-
file = "rescues/#{wrapper.rescue_template}"
|
|
90
|
-
|
|
91
|
-
if request.xhr?
|
|
92
|
-
body = template.render(template: file, layout: false, formats: [ :text ])
|
|
93
|
-
format = "text/plain"
|
|
94
|
-
else
|
|
95
|
-
body = template.render(template: file, layout: 'rescues/layout')
|
|
96
|
-
format = "text/html"
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
[ wrapper.status_code, { 'Content-Type' => "#{format}; charset=#{Response.default_charset}", 'Content-Length' => body.bytesize.to_s }, [ body ] ]
|
|
100
|
-
else
|
|
101
|
-
raise exception
|
|
102
|
-
end
|
|
103
|
-
end
|
|
104
|
-
end
|
|
105
|
-
end
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
module ActionDispatch
|
|
2
|
-
class ExceptionWrapper
|
|
3
|
-
def traces
|
|
4
|
-
appplication_trace_with_ids = []
|
|
5
|
-
framework_trace_with_ids = []
|
|
6
|
-
full_trace_with_ids = []
|
|
7
|
-
|
|
8
|
-
if full_trace
|
|
9
|
-
full_trace.each_with_index do |trace, idx|
|
|
10
|
-
trace_with_id = { id: idx, trace: trace }
|
|
11
|
-
|
|
12
|
-
appplication_trace_with_ids << trace_with_id if application_trace.include?(trace)
|
|
13
|
-
framework_trace_with_ids << trace_with_id if framework_trace.include?(trace)
|
|
14
|
-
full_trace_with_ids << trace_with_id
|
|
15
|
-
end
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
{
|
|
19
|
-
"Application Trace" => appplication_trace_with_ids,
|
|
20
|
-
"Framework Trace" => framework_trace_with_ids,
|
|
21
|
-
"Full Trace" => full_trace_with_ids
|
|
22
|
-
}
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def extract_sources
|
|
26
|
-
exception.backtrace.map do |trace|
|
|
27
|
-
file, line = trace.split(":")
|
|
28
|
-
line_number = line.to_i
|
|
29
|
-
|
|
30
|
-
{
|
|
31
|
-
code: source_fragment(file, line_number) || {},
|
|
32
|
-
file: file,
|
|
33
|
-
line_number: line_number
|
|
34
|
-
}
|
|
35
|
-
end if exception.backtrace
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
end
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
<% unless @exception.blamed_files.blank? %>
|
|
2
|
-
<% if (hide = @exception.blamed_files.length > 8) %>
|
|
3
|
-
<a href="#" onclick="return toggleTrace()">Toggle blamed files</a>
|
|
4
|
-
<% end %>
|
|
5
|
-
<pre id="blame_trace" <%='style="display:none"' if hide %>><code><%= @exception.describe_blame %></code></pre>
|
|
6
|
-
<% end %>
|
|
7
|
-
|
|
8
|
-
<%
|
|
9
|
-
clean_params = @request.filtered_parameters.clone
|
|
10
|
-
clean_params.delete("action")
|
|
11
|
-
clean_params.delete("controller")
|
|
12
|
-
|
|
13
|
-
request_dump = clean_params.empty? ? 'None' : clean_params.inspect.gsub(',', ",\n")
|
|
14
|
-
|
|
15
|
-
def debug_hash(object)
|
|
16
|
-
object.to_hash.sort_by { |k, _| k.to_s }.map { |k, v| "#{k}: #{v.inspect rescue $!.message}" }.join("\n")
|
|
17
|
-
end unless self.class.method_defined?(:debug_hash)
|
|
18
|
-
%>
|
|
19
|
-
|
|
20
|
-
<h2 style="margin-top: 30px">Request</h2>
|
|
21
|
-
<p><b>Parameters</b>:</p> <pre><%= request_dump %></pre>
|
|
22
|
-
|
|
23
|
-
<div class="details">
|
|
24
|
-
<div class="summary"><a href="#" onclick="return toggleSessionDump()">Toggle session dump</a></div>
|
|
25
|
-
<div id="session_dump" style="display:none"><pre><%= debug_hash @request.session %></pre></div>
|
|
26
|
-
</div>
|
|
27
|
-
|
|
28
|
-
<div class="details">
|
|
29
|
-
<div class="summary"><a href="#" onclick="return toggleEnvDump()">Toggle env dump</a></div>
|
|
30
|
-
<div id="env_dump" style="display:none"><pre><%= debug_hash @request.env.slice(*@request.class::ENV_METHODS) %></pre></div>
|
|
31
|
-
</div>
|
|
32
|
-
|
|
33
|
-
<h2 style="margin-top: 30px">Response</h2>
|
|
34
|
-
<p><b>Headers</b>:</p> <pre><%= defined?(@response) ? @response.headers.inspect.gsub(',', ",\n") : 'None' %></pre>
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
<%
|
|
2
|
-
clean_params = @request.filtered_parameters.clone
|
|
3
|
-
clean_params.delete("action")
|
|
4
|
-
clean_params.delete("controller")
|
|
5
|
-
|
|
6
|
-
request_dump = clean_params.empty? ? 'None' : clean_params.inspect.gsub(',', ",\n")
|
|
7
|
-
|
|
8
|
-
def debug_hash(object)
|
|
9
|
-
object.to_hash.sort_by { |k, _| k.to_s }.map { |k, v| "#{k}: #{v.inspect rescue $!.message}" }.join("\n")
|
|
10
|
-
end unless self.class.method_defined?(:debug_hash)
|
|
11
|
-
%>
|
|
12
|
-
|
|
13
|
-
Request parameters
|
|
14
|
-
<%= request_dump %>
|
|
15
|
-
|
|
16
|
-
Session dump
|
|
17
|
-
<%= debug_hash @request.session %>
|
|
18
|
-
|
|
19
|
-
Env dump
|
|
20
|
-
<%= debug_hash @request.env.slice(*@request.class::ENV_METHODS) %>
|
|
21
|
-
|
|
22
|
-
Response headers
|
|
23
|
-
<%= defined?(@response) ? @response.headers.inspect.gsub(',', ",\n") : 'None' %>
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
<% if @source_extract %>
|
|
2
|
-
<% @source_extract.each_with_index do |extract_source, index| %>
|
|
3
|
-
<% if extract_source[:code] %>
|
|
4
|
-
<div class="source <%="hidden" if @show_source_idx != index%>" id="frame-source-<%=index%>">
|
|
5
|
-
<div class="info">
|
|
6
|
-
Extracted source (around line <strong>#<%= extract_source[:line_number] %></strong>):
|
|
7
|
-
</div>
|
|
8
|
-
<div class="data">
|
|
9
|
-
<table cellpadding="0" cellspacing="0" class="lines">
|
|
10
|
-
<tr>
|
|
11
|
-
<td>
|
|
12
|
-
<pre class="line_numbers">
|
|
13
|
-
<% extract_source[:code].each_key do |line_number| %>
|
|
14
|
-
<span><%= line_number -%></span>
|
|
15
|
-
<% end %>
|
|
16
|
-
</pre>
|
|
17
|
-
</td>
|
|
18
|
-
<td width="100%">
|
|
19
|
-
<pre>
|
|
20
|
-
<% extract_source[:code].each do |line, source| -%><div class="line<%= " active" if line == extract_source[:line_number] -%>"><%= source -%></div><% end -%>
|
|
21
|
-
</pre>
|
|
22
|
-
</td>
|
|
23
|
-
</tr>
|
|
24
|
-
</table>
|
|
25
|
-
</div>
|
|
26
|
-
</div>
|
|
27
|
-
<% end %>
|
|
28
|
-
<% end %>
|
|
29
|
-
<% end %>
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
<% names = @traces.keys %>
|
|
2
|
-
|
|
3
|
-
<p><code>Rails.root: <%= defined?(Rails) && Rails.respond_to?(:root) ? Rails.root : "unset" %></code></p>
|
|
4
|
-
|
|
5
|
-
<div id="traces">
|
|
6
|
-
<% names.each do |name| %>
|
|
7
|
-
<%
|
|
8
|
-
show = "show('#{name.gsub(/\s/, '-')}');"
|
|
9
|
-
hide = (names - [name]).collect {|hide_name| "hide('#{hide_name.gsub(/\s/, '-')}');"}
|
|
10
|
-
%>
|
|
11
|
-
<a href="#" onclick="<%= hide.join %><%= show %>; return false;"><%= name %></a> <%= '|' unless names.last == name %>
|
|
12
|
-
<% end %>
|
|
13
|
-
|
|
14
|
-
<% @traces.each do |name, trace| %>
|
|
15
|
-
<div id="<%= name.gsub(/\s/, '-') %>" style="display: <%= (name == @trace_to_show) ? 'block' : 'none' %>;">
|
|
16
|
-
<pre><code><% trace.each do |frame| %><a class="trace-frames" data-frame-id="<%= frame[:id] %>" href="#"><%= frame[:trace] %></a><br><% end %></code></pre>
|
|
17
|
-
</div>
|
|
18
|
-
<% end %>
|
|
19
|
-
|
|
20
|
-
<script type="text/javascript">
|
|
21
|
-
var traceFrames = document.getElementsByClassName('trace-frames');
|
|
22
|
-
var selectedFrame, currentSource = document.getElementById('frame-source-0');
|
|
23
|
-
|
|
24
|
-
// Add click listeners for all stack frames
|
|
25
|
-
for (var i = 0; i < traceFrames.length; i++) {
|
|
26
|
-
traceFrames[i].addEventListener('click', function(e) {
|
|
27
|
-
e.preventDefault();
|
|
28
|
-
var target = e.target;
|
|
29
|
-
var frameId = target.dataset.frameId;
|
|
30
|
-
|
|
31
|
-
// Change the binding of the console.
|
|
32
|
-
changeBinding(frameId, function() {
|
|
33
|
-
if (selectedFrame) {
|
|
34
|
-
selectedFrame.className = selectedFrame.className.replace("selected", "");
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
target.className += " selected";
|
|
38
|
-
selectedFrame = target;
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
// Change the extracted source code
|
|
42
|
-
changeSourceExtract(frameId);
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function changeBinding(frameId, callback) {
|
|
47
|
-
var consoleEl = document.getElementById('console');
|
|
48
|
-
if (! consoleEl) { return; }
|
|
49
|
-
var url = consoleEl.dataset.remotePath + "/trace";
|
|
50
|
-
var params = "frame_id=" + encodeURIComponent(frameId);
|
|
51
|
-
var xhr = new XMLHttpRequest();
|
|
52
|
-
xhr.open("POST", url, true);
|
|
53
|
-
xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
|
|
54
|
-
xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
|
|
55
|
-
xhr.send(params);
|
|
56
|
-
xhr.onreadystatechange = function() {
|
|
57
|
-
if (xhr.readyState == 4 && xhr.status == 200) {
|
|
58
|
-
callback();
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function changeSourceExtract(frameId) {
|
|
64
|
-
var el = document.getElementById('frame-source-' + frameId);
|
|
65
|
-
if (currentSource && el) {
|
|
66
|
-
currentSource.className += " hidden";
|
|
67
|
-
el.className = el.className.replace(" hidden", "");
|
|
68
|
-
currentSource = el;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
</script>
|
|
72
|
-
</div>
|
|
@@ -1,420 +0,0 @@
|
|
|
1
|
-
<% if @console_session && @console_session.binding %>
|
|
2
|
-
<div id="console"
|
|
3
|
-
data-remote-path='<%= "console/repl_sessions/#{@console_session.id}" %>'
|
|
4
|
-
data-initial-prompt='<%= @console_session.prompt %>'>
|
|
5
|
-
</div>
|
|
6
|
-
|
|
7
|
-
<script type="text/javascript">
|
|
8
|
-
(function() {
|
|
9
|
-
// DOM helpers
|
|
10
|
-
function hasClass(el, className) {
|
|
11
|
-
var regex = new RegExp('(?:^|\\s)' + className + '(?!\\S)', 'g');
|
|
12
|
-
return el.className.match(regex);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function addClass(el, className) {
|
|
16
|
-
el.className += " " + className;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function removeClass(el, className) {
|
|
20
|
-
var regex = new RegExp('(?:^|\\s)' + className + '(?!\\S)', 'g');
|
|
21
|
-
el.className = el.className.replace(regex, '');
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function removeAllChildren(el) {
|
|
25
|
-
while (el.firstChild) {
|
|
26
|
-
el.removeChild(el.firstChild);
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function escapeHTML(html) {
|
|
31
|
-
return html
|
|
32
|
-
.replace(/&/g, '&')
|
|
33
|
-
.replace(/</g, '<')
|
|
34
|
-
.replace(/>/g, '>')
|
|
35
|
-
.replace(/"/g, '"')
|
|
36
|
-
.replace(/'/g, ''')
|
|
37
|
-
.replace(/`/g, '`');
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Add CSS styles dynamically. This probably doesnt work for IE <8.
|
|
41
|
-
var style = document.createElement('style');
|
|
42
|
-
style.type = 'text/css';
|
|
43
|
-
style.innerHTML =
|
|
44
|
-
"#console { position: fixed; left: 0; bottom: 0; width: 100%; height: 150px; border: 1px solid; padding: 0 0 20px 0; overflow: none; margin: 0; background: none repeat scroll 0% 0% #333; } " +
|
|
45
|
-
"#console div.console-inner { font-family: monospace; font-size: 11px; height: 100%; overflow: auto; background: #333; border: 0; padding: 0 margin: 0; } " +
|
|
46
|
-
"#console div.console-prompt-box { color: #FFF; } " +
|
|
47
|
-
"#console pre.console-message { color: #1AD027; margin: 0; border: 0; white-space: pre-wrap; background-color: #333; padding: 0; } " +
|
|
48
|
-
"#console div.console-focus span.console-cursor { background: #FEFEFE; color: #333; font-weight: bold; }";
|
|
49
|
-
document.getElementsByTagName('head')[0].appendChild(style);
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Constructor for command storage.
|
|
53
|
-
* It uses localStorage if available. Otherwise fallback to normal JS array.
|
|
54
|
-
*/
|
|
55
|
-
function CommandStorage() {
|
|
56
|
-
this.previousCommands = [];
|
|
57
|
-
var previousCommandOffset = 0;
|
|
58
|
-
var hasLocalStorage = typeof window.localStorage !== 'undefined';
|
|
59
|
-
var STORAGE_KEY = "web_console_previous_commands";
|
|
60
|
-
var MAX_STORAGE = 100;
|
|
61
|
-
|
|
62
|
-
if (hasLocalStorage) {
|
|
63
|
-
this.previousCommands = JSON.parse(localStorage.getItem(STORAGE_KEY)) || [];
|
|
64
|
-
previousCommandOffset = this.previousCommands.length;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
this.addCommand = function(command) {
|
|
68
|
-
previousCommandOffset = this.previousCommands.push(command);
|
|
69
|
-
|
|
70
|
-
if (previousCommandOffset > MAX_STORAGE) {
|
|
71
|
-
this.previousCommands.splice(0, 1);
|
|
72
|
-
previousCommandOffset = MAX_STORAGE;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
if (hasLocalStorage) {
|
|
76
|
-
localStorage.setItem(STORAGE_KEY, JSON.stringify(this.previousCommands));
|
|
77
|
-
}
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
this.navigate = function(offset) {
|
|
81
|
-
previousCommandOffset += offset;
|
|
82
|
-
|
|
83
|
-
if (previousCommandOffset < 0) {
|
|
84
|
-
previousCommandOffset = -1;
|
|
85
|
-
return null;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
if (previousCommandOffset >= this.previousCommands.length) {
|
|
89
|
-
previousCommandOffset = this.previousCommands.length;
|
|
90
|
-
return null;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
return this.previousCommands[previousCommandOffset];
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// HTML strings for dynamic elements.
|
|
98
|
-
var consoleInnerHtml = [
|
|
99
|
-
"<div id='resizer' style='width: 100%; height: 3px; cursor: ns-resize;'></div>",
|
|
100
|
-
"<div class='console-inner'></div>",
|
|
101
|
-
"<input id='clipboard' type='text' style='visibility: hidden;'>"
|
|
102
|
-
].join("");
|
|
103
|
-
|
|
104
|
-
var promptBoxHtml = [
|
|
105
|
-
"<span class='console-prompt-label' style='display: inline; color: #FFF; background: none repeat scroll 0% 0% #333; border: 0; padding: 0;'></span>",
|
|
106
|
-
"<pre class='console-prompt-display' style='display: inline; color: #FFF; background: none repeat scroll 0% 0% #333; border: 0; padding: 0;'></pre>"
|
|
107
|
-
].join("");
|
|
108
|
-
|
|
109
|
-
// REPLConsole Constructor
|
|
110
|
-
function REPLConsole(config) {
|
|
111
|
-
this.commandStorage = new CommandStorage();
|
|
112
|
-
this.prompt = config && config.promptLabel ? config.promptLabel : ' >>';
|
|
113
|
-
this.commandHandle = config && config.commandHandle ? config.commandHandle : function() { return this; }
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
REPLConsole.prototype.install = function(container) {
|
|
118
|
-
var _this = this;
|
|
119
|
-
|
|
120
|
-
document.onkeydown = function(ev) {
|
|
121
|
-
if (_this.focused) { _this.onKeyDown(ev); }
|
|
122
|
-
};
|
|
123
|
-
|
|
124
|
-
document.onkeypress = function(ev) {
|
|
125
|
-
if (_this.focused) { _this.onKeyPress(ev); }
|
|
126
|
-
};
|
|
127
|
-
|
|
128
|
-
document.addEventListener('mousedown', function(ev) {
|
|
129
|
-
var el = ev.target || ev.srcElement;
|
|
130
|
-
|
|
131
|
-
if (el) {
|
|
132
|
-
do {
|
|
133
|
-
if (el === container) {
|
|
134
|
-
_this.focus();
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
} while (el = el.parentNode);
|
|
138
|
-
|
|
139
|
-
_this.blur();
|
|
140
|
-
}
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
// Render the console.
|
|
144
|
-
container.innerHTML = consoleInnerHtml;
|
|
145
|
-
// Make the console resizable.
|
|
146
|
-
document.getElementById('resizer').addEventListener('mousedown', function(ev) {
|
|
147
|
-
var startY = ev.clientY;
|
|
148
|
-
var startHeight = parseInt(document.defaultView.getComputedStyle(consoleDiv).height, 10);
|
|
149
|
-
|
|
150
|
-
var doDrag = function(e) {
|
|
151
|
-
consoleDiv.style.height = (startHeight + startY - e.clientY) + 'px';
|
|
152
|
-
};
|
|
153
|
-
|
|
154
|
-
var stopDrag = function(e) {
|
|
155
|
-
document.documentElement.removeEventListener('mousemove', doDrag, false);
|
|
156
|
-
document.documentElement.removeEventListener('mouseup', stopDrag, false);
|
|
157
|
-
};
|
|
158
|
-
|
|
159
|
-
document.documentElement.addEventListener('mousemove', doDrag, false);
|
|
160
|
-
document.documentElement.addEventListener('mouseup', stopDrag, false);
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
// Initialize
|
|
164
|
-
this.inner = container.getElementsByClassName('console-inner')[0];
|
|
165
|
-
this.clipboard = document.getElementById('clipboard');
|
|
166
|
-
this.newPromptBox();
|
|
167
|
-
this.focus();
|
|
168
|
-
};
|
|
169
|
-
|
|
170
|
-
REPLConsole.prototype.focus = function() {
|
|
171
|
-
if (! this.focused) {
|
|
172
|
-
this.focused = true;
|
|
173
|
-
if (! hasClass(this.inner, "console-focus")) {
|
|
174
|
-
addClass(this.inner, "console-focus");
|
|
175
|
-
}
|
|
176
|
-
this.scrollToBottom();
|
|
177
|
-
}
|
|
178
|
-
};
|
|
179
|
-
|
|
180
|
-
REPLConsole.prototype.blur = function() {
|
|
181
|
-
this.focused = false;
|
|
182
|
-
removeClass(this.inner, "console-focus");
|
|
183
|
-
};
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* Add a new empty prompt box to the console.
|
|
187
|
-
*/
|
|
188
|
-
REPLConsole.prototype.newPromptBox = function() {
|
|
189
|
-
// Remove the caret from previous prompt display if any.
|
|
190
|
-
if (this.promptDisplay) {
|
|
191
|
-
this.removeCaretFromPrompt();
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
var promptBox = document.createElement('div');
|
|
195
|
-
promptBox.className = "console-prompt-box";
|
|
196
|
-
promptBox.innerHTML = promptBoxHtml;
|
|
197
|
-
this.promptLabel = promptBox.getElementsByClassName('console-prompt-label')[0];
|
|
198
|
-
this.promptDisplay = promptBox.getElementsByClassName('console-prompt-display')[0];
|
|
199
|
-
// Render the prompt box
|
|
200
|
-
this.setInput("");
|
|
201
|
-
this.promptLabel.innerHTML = this.prompt;
|
|
202
|
-
this.inner.appendChild(promptBox);
|
|
203
|
-
this.scrollToBottom();
|
|
204
|
-
};
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* Remove the caret from the prompt box,
|
|
208
|
-
* mainly before adding a new prompt box.
|
|
209
|
-
* For simplicity, just re-render the prompt box
|
|
210
|
-
* with caret position -1.
|
|
211
|
-
*/
|
|
212
|
-
REPLConsole.prototype.removeCaretFromPrompt = function() {
|
|
213
|
-
this.setInput(this._input, -1);
|
|
214
|
-
};
|
|
215
|
-
|
|
216
|
-
REPLConsole.prototype.setInput = function(input, caretPos) {
|
|
217
|
-
this._caretPos = caretPos === undefined ? input.length : caretPos;
|
|
218
|
-
this._input = input;
|
|
219
|
-
this.renderInput();
|
|
220
|
-
};
|
|
221
|
-
|
|
222
|
-
/**
|
|
223
|
-
* Add some text to the existing input.
|
|
224
|
-
*/
|
|
225
|
-
REPLConsole.prototype.addToInput = function(val, caretPos) {
|
|
226
|
-
caretPos = caretPos || this._caretPos;
|
|
227
|
-
var before = this._input.substring(0, caretPos);
|
|
228
|
-
var after = this._input.substring(caretPos, this._input.length);
|
|
229
|
-
var newInput = before + val + after;
|
|
230
|
-
this.setInput(newInput, caretPos + val.length);
|
|
231
|
-
};
|
|
232
|
-
|
|
233
|
-
/**
|
|
234
|
-
* Render the input prompt. This is called whenever
|
|
235
|
-
* the user input changes, sometimes not very efficient.
|
|
236
|
-
*/
|
|
237
|
-
REPLConsole.prototype.renderInput = function() {
|
|
238
|
-
// Clear the current input.
|
|
239
|
-
removeAllChildren(this.promptDisplay);
|
|
240
|
-
|
|
241
|
-
var promptCursor = document.createElement('span');
|
|
242
|
-
promptCursor.className = "console-cursor";
|
|
243
|
-
var before, current, after;
|
|
244
|
-
|
|
245
|
-
if (this._caretPos < 0) {
|
|
246
|
-
before = this._input;
|
|
247
|
-
current = after = "";
|
|
248
|
-
} else if (this._caretPos === this._input.length) {
|
|
249
|
-
before = this._input;
|
|
250
|
-
current = "\u00A0";
|
|
251
|
-
after = "";
|
|
252
|
-
} else {
|
|
253
|
-
before = this._input.substring(0, this._caretPos);
|
|
254
|
-
current = this._input.charAt(this._caretPos);
|
|
255
|
-
after = this._input.substring(this._caretPos + 1, this._input.length);
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
this.promptDisplay.appendChild(document.createTextNode(before));
|
|
259
|
-
promptCursor.appendChild(document.createTextNode(current));
|
|
260
|
-
this.promptDisplay.appendChild(promptCursor);
|
|
261
|
-
this.promptDisplay.appendChild(document.createTextNode(after));
|
|
262
|
-
};
|
|
263
|
-
|
|
264
|
-
REPLConsole.prototype.writeOutput = function(output) {
|
|
265
|
-
var consoleMessage = document.createElement('pre');
|
|
266
|
-
consoleMessage.className = "console-message";
|
|
267
|
-
consoleMessage.innerHTML = escapeHTML(output);
|
|
268
|
-
this.inner.appendChild(consoleMessage);
|
|
269
|
-
this.newPromptBox();
|
|
270
|
-
};
|
|
271
|
-
|
|
272
|
-
REPLConsole.prototype.onEnterKey = function() {
|
|
273
|
-
var input = this._input;
|
|
274
|
-
|
|
275
|
-
if(input != "" && input !== undefined) {
|
|
276
|
-
this.commandStorage.addCommand(input);
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
this.commandHandle(input);
|
|
280
|
-
};
|
|
281
|
-
|
|
282
|
-
REPLConsole.prototype.onNavigateHistory = function(offset) {
|
|
283
|
-
var command = this.commandStorage.navigate(offset) || "";
|
|
284
|
-
this.setInput(command);
|
|
285
|
-
};
|
|
286
|
-
|
|
287
|
-
/**
|
|
288
|
-
* Handle control keys like up, down, left, right.
|
|
289
|
-
*/
|
|
290
|
-
REPLConsole.prototype.onKeyDown = function(ev) {
|
|
291
|
-
switch (ev.keyCode) {
|
|
292
|
-
case 13:
|
|
293
|
-
// Enter key
|
|
294
|
-
this.onEnterKey();
|
|
295
|
-
ev.preventDefault();
|
|
296
|
-
break;
|
|
297
|
-
case 80:
|
|
298
|
-
// Ctrl-P
|
|
299
|
-
if (! ev.ctrlKey) break;
|
|
300
|
-
case 38:
|
|
301
|
-
// Up arrow
|
|
302
|
-
this.onNavigateHistory(-1);
|
|
303
|
-
ev.preventDefault();
|
|
304
|
-
break;
|
|
305
|
-
case 78:
|
|
306
|
-
// Ctrl-N
|
|
307
|
-
if (! ev.ctrlKey) break;
|
|
308
|
-
case 40:
|
|
309
|
-
// Down arrow
|
|
310
|
-
this.onNavigateHistory(1);
|
|
311
|
-
ev.preventDefault();
|
|
312
|
-
break;
|
|
313
|
-
case 37:
|
|
314
|
-
// Left arrow
|
|
315
|
-
var caretPos = this._caretPos > 0 ? this._caretPos - 1 : this._caretPos;
|
|
316
|
-
this.setInput(this._input, caretPos);
|
|
317
|
-
ev.preventDefault();
|
|
318
|
-
break;
|
|
319
|
-
case 39:
|
|
320
|
-
// Right arrow
|
|
321
|
-
var length = this._input.length;
|
|
322
|
-
var caretPos = this._caretPos < length ? this._caretPos + 1 : this._caretPos;
|
|
323
|
-
this.setInput(this._input, caretPos);
|
|
324
|
-
ev.preventDefault();
|
|
325
|
-
break;
|
|
326
|
-
case 8:
|
|
327
|
-
// Delete
|
|
328
|
-
this.deleteAtCurrent();
|
|
329
|
-
ev.preventDefault();
|
|
330
|
-
break;
|
|
331
|
-
case 86:
|
|
332
|
-
// ctrl/command-v
|
|
333
|
-
if (ev.ctrlKey || ev.metaKey) {
|
|
334
|
-
// Let the pasted text go to our clipboard.
|
|
335
|
-
this.clipboard.focus();
|
|
336
|
-
|
|
337
|
-
// Pasting to clipboard doesn't happen immediately,
|
|
338
|
-
// so we have to wait for a while to get the pasted text.
|
|
339
|
-
var _this = this;
|
|
340
|
-
setTimeout(function() {
|
|
341
|
-
_this.addToInput(_this.clipboard.value);
|
|
342
|
-
_this.clipboard.value = "";
|
|
343
|
-
_this.clipboard.blur();
|
|
344
|
-
}, 10);
|
|
345
|
-
}
|
|
346
|
-
break;
|
|
347
|
-
default:
|
|
348
|
-
break;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
ev.stopPropagation();
|
|
352
|
-
};
|
|
353
|
-
|
|
354
|
-
/**
|
|
355
|
-
* Handle input key press.
|
|
356
|
-
*/
|
|
357
|
-
REPLConsole.prototype.onKeyPress = function(ev) {
|
|
358
|
-
// Only write to the console if it's a single key press.
|
|
359
|
-
if (ev.ctrlKey || ev.metaKey) { return; }
|
|
360
|
-
var keyCode = ev.keyCode || ev.which;
|
|
361
|
-
this.insertAtCurrent(String.fromCharCode(keyCode));
|
|
362
|
-
ev.stopPropagation();
|
|
363
|
-
ev.preventDefault();
|
|
364
|
-
};
|
|
365
|
-
|
|
366
|
-
/**
|
|
367
|
-
* Delete a character at the current position.
|
|
368
|
-
*/
|
|
369
|
-
REPLConsole.prototype.deleteAtCurrent = function() {
|
|
370
|
-
if (this._caretPos > 0) {
|
|
371
|
-
var caretPos = this._caretPos - 1;
|
|
372
|
-
var before = this._input.substring(0, caretPos);
|
|
373
|
-
var after = this._input.substring(this._caretPos, this._input.length);
|
|
374
|
-
this.setInput(before + after, caretPos);
|
|
375
|
-
}
|
|
376
|
-
};
|
|
377
|
-
|
|
378
|
-
/**
|
|
379
|
-
* Insert a character at the current position.
|
|
380
|
-
*/
|
|
381
|
-
REPLConsole.prototype.insertAtCurrent = function(char) {
|
|
382
|
-
var before = this._input.substring(0, this._caretPos);
|
|
383
|
-
var after = this._input.substring(this._caretPos, this._input.length);
|
|
384
|
-
this.setInput(before + char + after, this._caretPos + 1);
|
|
385
|
-
};
|
|
386
|
-
|
|
387
|
-
REPLConsole.prototype.scrollToBottom = function() {
|
|
388
|
-
this.inner.scrollTop = this.inner.scrollHeight;
|
|
389
|
-
};
|
|
390
|
-
|
|
391
|
-
window.REPLConsole = REPLConsole;
|
|
392
|
-
})();
|
|
393
|
-
|
|
394
|
-
// Install the console
|
|
395
|
-
var consoleDiv = document.getElementById('console');
|
|
396
|
-
var replConsole = new REPLConsole({
|
|
397
|
-
promptLabel: consoleDiv.dataset.initialPrompt,
|
|
398
|
-
commandHandle: function(line) {
|
|
399
|
-
var _this = this;
|
|
400
|
-
var xhr = new XMLHttpRequest();
|
|
401
|
-
var url = consoleDiv.dataset.remotePath;
|
|
402
|
-
var params = "input=" + encodeURIComponent(line);
|
|
403
|
-
|
|
404
|
-
xhr.open("PUT", url, true);
|
|
405
|
-
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
|
|
406
|
-
xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
|
|
407
|
-
xhr.send(params);
|
|
408
|
-
|
|
409
|
-
xhr.onreadystatechange = function() {
|
|
410
|
-
if(xhr.readyState == 4 && xhr.status == 200) {
|
|
411
|
-
var response = JSON.parse(xhr.responseText);
|
|
412
|
-
_this.writeOutput(response.output);
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
});
|
|
417
|
-
|
|
418
|
-
replConsole.install(consoleDiv);
|
|
419
|
-
</script>
|
|
420
|
-
<% end %>
|