better_errors 2.6.0 → 2.10.1
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/.github/release-drafter.yml +46 -0
- data/.github/workflows/ci.yml +123 -0
- data/.github/workflows/draft_release_update.yml +22 -0
- data/.github/workflows/pull_request.yml +20 -0
- data/.github/workflows/release.yml +68 -0
- data/.gitignore +4 -0
- data/.ruby-version +1 -0
- data/Gemfile +6 -1
- data/{LICENSE.txt → LICENSE} +1 -1
- data/README.md +34 -2
- data/better_errors.gemspec +8 -6
- data/gemfiles/pry010.gemfile +2 -1
- data/gemfiles/pry011.gemfile +2 -1
- data/gemfiles/pry09.gemfile +2 -1
- data/gemfiles/rack.gemfile +2 -1
- data/gemfiles/rack_boc.gemfile +2 -1
- data/gemfiles/rails42.gemfile +2 -1
- data/gemfiles/rails42_boc.gemfile +2 -1
- data/gemfiles/rails42_haml.gemfile +2 -1
- data/gemfiles/rails50.gemfile +2 -1
- data/gemfiles/rails50_boc.gemfile +2 -1
- data/gemfiles/rails50_haml.gemfile +2 -1
- data/gemfiles/rails51.gemfile +2 -1
- data/gemfiles/rails51_boc.gemfile +2 -1
- data/gemfiles/rails51_haml.gemfile +2 -1
- data/gemfiles/rails52.gemfile +2 -1
- data/gemfiles/rails52_boc.gemfile +2 -1
- data/gemfiles/rails52_haml.gemfile +2 -1
- data/gemfiles/rails60.gemfile +8 -0
- data/gemfiles/rails60_boc.gemfile +9 -0
- data/gemfiles/rails60_haml.gemfile +9 -0
- data/gemfiles/rails61.gemfile +8 -0
- data/gemfiles/rails61_boc.gemfile +9 -0
- data/gemfiles/rails61_haml.gemfile +9 -0
- data/lib/better_errors/code_formatter/html.rb +15 -1
- data/lib/better_errors/code_formatter.rb +16 -27
- data/lib/better_errors/editor.rb +103 -0
- data/lib/better_errors/error_page.rb +53 -12
- data/lib/better_errors/error_page_style.rb +43 -0
- data/lib/better_errors/exception_hint.rb +29 -0
- data/lib/better_errors/middleware.rb +75 -12
- data/lib/better_errors/raised_exception.rb +25 -4
- data/lib/better_errors/templates/main.css +1 -0
- data/lib/better_errors/templates/main.erb +94 -709
- data/lib/better_errors/templates/text.erb +6 -3
- data/lib/better_errors/templates/variable_info.erb +24 -14
- data/lib/better_errors/version.rb +2 -1
- data/lib/better_errors.rb +20 -34
- metadata +51 -8
- data/.travis.yml +0 -58
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
require "rouge"
|
|
2
|
+
|
|
1
3
|
module BetterErrors
|
|
2
4
|
# @private
|
|
3
5
|
class CodeFormatter::HTML < CodeFormatter
|
|
@@ -20,7 +22,19 @@ module BetterErrors
|
|
|
20
22
|
end
|
|
21
23
|
|
|
22
24
|
def formatted_code
|
|
23
|
-
%{
|
|
25
|
+
%{
|
|
26
|
+
<div class="code_linenums">#{formatted_nums.join}</div>
|
|
27
|
+
<div class="code"><div class='code-wrapper'>#{super}</div></div>
|
|
28
|
+
}
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def rouge_lexer
|
|
32
|
+
Rouge::Lexer.guess(filename: filename, source: source) { Rouge::Lexers::Ruby }
|
|
24
33
|
end
|
|
34
|
+
|
|
35
|
+
def highlighted_lines
|
|
36
|
+
Rouge::Formatters::HTML.new.format(rouge_lexer.lex(context_lines.join)).lines
|
|
37
|
+
end
|
|
38
|
+
|
|
25
39
|
end
|
|
26
40
|
end
|
|
@@ -4,14 +4,6 @@ module BetterErrors
|
|
|
4
4
|
require "better_errors/code_formatter/html"
|
|
5
5
|
require "better_errors/code_formatter/text"
|
|
6
6
|
|
|
7
|
-
FILE_TYPES = {
|
|
8
|
-
".rb" => :ruby,
|
|
9
|
-
"" => :ruby,
|
|
10
|
-
".html" => :html,
|
|
11
|
-
".erb" => :erb,
|
|
12
|
-
".haml" => :haml
|
|
13
|
-
}
|
|
14
|
-
|
|
15
7
|
attr_reader :filename, :line, :context
|
|
16
8
|
|
|
17
9
|
def initialize(filename, line, context = 5)
|
|
@@ -26,13 +18,21 @@ module BetterErrors
|
|
|
26
18
|
source_unavailable
|
|
27
19
|
end
|
|
28
20
|
|
|
29
|
-
def
|
|
30
|
-
|
|
21
|
+
def line_range
|
|
22
|
+
min = [line - context, 1].max
|
|
23
|
+
max = [line + context, source_lines.count].min
|
|
24
|
+
min..max
|
|
31
25
|
end
|
|
32
26
|
|
|
33
|
-
def
|
|
34
|
-
|
|
35
|
-
|
|
27
|
+
def context_lines
|
|
28
|
+
range = line_range
|
|
29
|
+
source_lines[(range.begin - 1)..(range.end - 1)] or raise Errno::EINVAL
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def formatted_code
|
|
35
|
+
formatted_lines.join
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
def each_line_of(lines, &blk)
|
|
@@ -41,23 +41,12 @@ module BetterErrors
|
|
|
41
41
|
}
|
|
42
42
|
end
|
|
43
43
|
|
|
44
|
-
def
|
|
45
|
-
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
def context_lines
|
|
49
|
-
range = line_range
|
|
50
|
-
source_lines[(range.begin - 1)..(range.end - 1)] or raise Errno::EINVAL
|
|
44
|
+
def source
|
|
45
|
+
@source ||= File.read(filename)
|
|
51
46
|
end
|
|
52
47
|
|
|
53
48
|
def source_lines
|
|
54
|
-
@source_lines ||=
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
def line_range
|
|
58
|
-
min = [line - context, 1].max
|
|
59
|
-
max = [line + context, source_lines.count].min
|
|
60
|
-
min..max
|
|
49
|
+
@source_lines ||= source.lines
|
|
61
50
|
end
|
|
62
51
|
end
|
|
63
52
|
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
require "uri"
|
|
2
|
+
|
|
3
|
+
module BetterErrors
|
|
4
|
+
class Editor
|
|
5
|
+
KNOWN_EDITORS = [
|
|
6
|
+
{ symbols: [:atom], sniff: /atom/i, url: "atom://core/open/file?filename=%{file}&line=%{line}" },
|
|
7
|
+
{ symbols: [:emacs, :emacsclient], sniff: /emacs/i, url: "emacs://open?url=file://%{file}&line=%{line}" },
|
|
8
|
+
{ symbols: [:idea], sniff: /idea/i, url: "idea://open?file=%{file}&line=%{line}" },
|
|
9
|
+
{ symbols: [:macvim, :mvim], sniff: /vim/i, url: "mvim://open?url=file://%{file_unencoded}&line=%{line}" },
|
|
10
|
+
{ symbols: [:rubymine], sniff: /mine/i, url: "x-mine://open?file=%{file}&line=%{line}" },
|
|
11
|
+
{ symbols: [:sublime, :subl, :st], sniff: /subl/i, url: "subl://open?url=file://%{file}&line=%{line}" },
|
|
12
|
+
{ symbols: [:textmate, :txmt, :tm], sniff: /mate/i, url: "txmt://open?url=file://%{file}&line=%{line}" },
|
|
13
|
+
{ symbols: [:vscode, :code], sniff: /code/i, url: "vscode://file/%{file}:%{line}" },
|
|
14
|
+
{ symbols: [:vscodium, :codium], sniff: /codium/i, url: "vscodium://file/%{file}:%{line}" },
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
def self.for_formatting_string(formatting_string)
|
|
18
|
+
new proc { |file, line|
|
|
19
|
+
formatting_string % { file: URI.encode_www_form_component(file), file_unencoded: file, line: line }
|
|
20
|
+
}
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.for_proc(url_proc)
|
|
24
|
+
new url_proc
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Automatically sniffs a default editor preset based on
|
|
28
|
+
# environment variables.
|
|
29
|
+
#
|
|
30
|
+
# @return [Symbol]
|
|
31
|
+
def self.default_editor
|
|
32
|
+
editor_from_environment_formatting_string ||
|
|
33
|
+
editor_from_environment_editor ||
|
|
34
|
+
editor_from_symbol(:textmate)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def self.editor_from_environment_editor
|
|
38
|
+
if ENV["BETTER_ERRORS_EDITOR"]
|
|
39
|
+
editor = editor_from_command(ENV["BETTER_ERRORS_EDITOR"])
|
|
40
|
+
return editor if editor
|
|
41
|
+
puts "BETTER_ERRORS_EDITOR environment variable is not recognized as a supported Better Errors editor."
|
|
42
|
+
end
|
|
43
|
+
if ENV["EDITOR"]
|
|
44
|
+
editor = editor_from_command(ENV["EDITOR"])
|
|
45
|
+
return editor if editor
|
|
46
|
+
puts "EDITOR environment variable is not recognized as a supported Better Errors editor. Using TextMate by default."
|
|
47
|
+
else
|
|
48
|
+
puts "Since there is no EDITOR or BETTER_ERRORS_EDITOR environment variable, using Textmate by default."
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def self.editor_from_command(editor_command)
|
|
53
|
+
env_preset = KNOWN_EDITORS.find { |preset| editor_command =~ preset[:sniff] }
|
|
54
|
+
for_formatting_string(env_preset[:url]) if env_preset
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def self.editor_from_environment_formatting_string
|
|
58
|
+
return unless ENV['BETTER_ERRORS_EDITOR_URL']
|
|
59
|
+
|
|
60
|
+
for_formatting_string(ENV['BETTER_ERRORS_EDITOR_URL'])
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def self.editor_from_symbol(symbol)
|
|
64
|
+
KNOWN_EDITORS.each do |preset|
|
|
65
|
+
return for_formatting_string(preset[:url]) if preset[:symbols].include?(symbol)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def initialize(url_proc)
|
|
70
|
+
@url_proc = url_proc
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def url(raw_path, line)
|
|
74
|
+
if virtual_path && raw_path.start_with?(virtual_path)
|
|
75
|
+
if host_path
|
|
76
|
+
file = raw_path.sub(%r{\A#{virtual_path}}, host_path)
|
|
77
|
+
else
|
|
78
|
+
file = raw_path.sub(%r{\A#{virtual_path}/}, '')
|
|
79
|
+
end
|
|
80
|
+
else
|
|
81
|
+
file = raw_path
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
url_proc.call(file, line)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def scheme
|
|
88
|
+
url('/fake', 42).sub(/:.*/, ':')
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
private
|
|
92
|
+
|
|
93
|
+
attr_reader :url_proc
|
|
94
|
+
|
|
95
|
+
def virtual_path
|
|
96
|
+
@virtual_path ||= ENV['BETTER_ERRORS_VIRTUAL_PATH']
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def host_path
|
|
100
|
+
@host_path ||= ENV['BETTER_ERRORS_HOST_PATH']
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
require "cgi"
|
|
2
2
|
require "json"
|
|
3
3
|
require "securerandom"
|
|
4
|
+
require "rouge"
|
|
5
|
+
require "better_errors/error_page_style"
|
|
4
6
|
|
|
5
7
|
module BetterErrors
|
|
6
8
|
# @private
|
|
7
9
|
class ErrorPage
|
|
10
|
+
VariableInfo = Struct.new(:frame, :editor_url, :rails_params, :rack_session, :start_time)
|
|
11
|
+
|
|
8
12
|
def self.template_path(template_name)
|
|
9
13
|
File.expand_path("../templates/#{template_name}.erb", __FILE__)
|
|
10
14
|
end
|
|
@@ -13,6 +17,15 @@ module BetterErrors
|
|
|
13
17
|
Erubi::Engine.new(File.read(template_path(template_name)), escape: true)
|
|
14
18
|
end
|
|
15
19
|
|
|
20
|
+
def self.render_template(template_name, locals)
|
|
21
|
+
locals.send(:eval, self.template(template_name).src)
|
|
22
|
+
rescue => e
|
|
23
|
+
# Fix the backtrace, which doesn't identify the template that failed (within Better Errors).
|
|
24
|
+
# We don't know the line number, so just injecting the template path has to be enough.
|
|
25
|
+
e.backtrace.unshift "#{self.template_path(template_name)}:0"
|
|
26
|
+
raise
|
|
27
|
+
end
|
|
28
|
+
|
|
16
29
|
attr_reader :exception, :env, :repls
|
|
17
30
|
|
|
18
31
|
def initialize(exception, env)
|
|
@@ -26,15 +39,21 @@ module BetterErrors
|
|
|
26
39
|
@id ||= SecureRandom.hex(8)
|
|
27
40
|
end
|
|
28
41
|
|
|
29
|
-
def
|
|
30
|
-
|
|
42
|
+
def render_main(csrf_token, csp_nonce)
|
|
43
|
+
frame = backtrace_frames[0]
|
|
44
|
+
first_frame_variable_info = VariableInfo.new(frame, editor_url(frame), rails_params, rack_session, Time.now.to_f)
|
|
45
|
+
self.class.render_template('main', binding)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def render_text
|
|
49
|
+
self.class.render_template('text', binding)
|
|
31
50
|
end
|
|
32
51
|
|
|
33
52
|
def do_variables(opts)
|
|
34
53
|
index = opts["index"].to_i
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
{ html:
|
|
54
|
+
frame = backtrace_frames[index]
|
|
55
|
+
variable_info = VariableInfo.new(frame, editor_url(frame), rails_params, rack_session, Time.now.to_f)
|
|
56
|
+
{ html: self.class.render_template("variable_info", variable_info) }
|
|
38
57
|
end
|
|
39
58
|
|
|
40
59
|
def do_eval(opts)
|
|
@@ -59,7 +78,23 @@ module BetterErrors
|
|
|
59
78
|
end
|
|
60
79
|
|
|
61
80
|
def exception_message
|
|
62
|
-
exception.message.
|
|
81
|
+
exception.message.strip.gsub(/(\r?\n\s*\r?\n)+/, "\n")
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def exception_hint
|
|
85
|
+
exception.hint
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def active_support_actions
|
|
89
|
+
return [] unless defined?(ActiveSupport::ActionableError)
|
|
90
|
+
|
|
91
|
+
ActiveSupport::ActionableError.actions(exception.type)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def action_dispatch_action_endpoint
|
|
95
|
+
return unless defined?(ActionDispatch::ActionableExceptions)
|
|
96
|
+
|
|
97
|
+
ActionDispatch::ActionableExceptions.endpoint
|
|
63
98
|
end
|
|
64
99
|
|
|
65
100
|
def application_frames
|
|
@@ -73,7 +108,7 @@ module BetterErrors
|
|
|
73
108
|
private
|
|
74
109
|
|
|
75
110
|
def editor_url(frame)
|
|
76
|
-
BetterErrors.editor
|
|
111
|
+
BetterErrors.editor.url(frame.filename, frame.line)
|
|
77
112
|
end
|
|
78
113
|
|
|
79
114
|
def rack_session
|
|
@@ -92,11 +127,11 @@ module BetterErrors
|
|
|
92
127
|
env["PATH_INFO"]
|
|
93
128
|
end
|
|
94
129
|
|
|
95
|
-
def html_formatted_code_block(frame)
|
|
130
|
+
def self.html_formatted_code_block(frame)
|
|
96
131
|
CodeFormatter::HTML.new(frame.filename, frame.line).output
|
|
97
132
|
end
|
|
98
133
|
|
|
99
|
-
def text_formatted_code_block(frame)
|
|
134
|
+
def self.text_formatted_code_block(frame)
|
|
100
135
|
CodeFormatter::Text.new(frame.filename, frame.line).output
|
|
101
136
|
end
|
|
102
137
|
|
|
@@ -104,8 +139,14 @@ module BetterErrors
|
|
|
104
139
|
str + "\n" + char*str.size
|
|
105
140
|
end
|
|
106
141
|
|
|
107
|
-
def inspect_value(obj)
|
|
108
|
-
|
|
142
|
+
def self.inspect_value(obj)
|
|
143
|
+
if BetterErrors.ignored_classes.include? obj.class.name
|
|
144
|
+
"<span class='unsupported'>(Instance of ignored class. "\
|
|
145
|
+
"#{obj.class.name ? "Remove #{CGI.escapeHTML(obj.class.name)} from" : "Modify"}"\
|
|
146
|
+
" BetterErrors.ignored_classes if you need to see it.)</span>"
|
|
147
|
+
else
|
|
148
|
+
InspectableValue.new(obj).to_html
|
|
149
|
+
end
|
|
109
150
|
rescue BetterErrors::ValueLargerThanConfiguredMaximum
|
|
110
151
|
"<span class='unsupported'>(Object too large. "\
|
|
111
152
|
"#{obj.class.name ? "Modify #{CGI.escapeHTML(obj.class.name)}#inspect or a" : "A"}"\
|
|
@@ -118,7 +159,7 @@ module BetterErrors
|
|
|
118
159
|
result, prompt, prefilled_input = @repls[index].send_input(code)
|
|
119
160
|
|
|
120
161
|
{
|
|
121
|
-
highlighted_input:
|
|
162
|
+
highlighted_input: Rouge::Formatters::HTML.new.format(Rouge::Lexers::Ruby.lex(code)),
|
|
122
163
|
prefilled_input: prefilled_input,
|
|
123
164
|
prompt: prompt,
|
|
124
165
|
result: result
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
module BetterErrors
|
|
2
|
+
# @private
|
|
3
|
+
module ErrorPageStyle
|
|
4
|
+
def self.compiled_css(for_deployment = false)
|
|
5
|
+
begin
|
|
6
|
+
require "sassc"
|
|
7
|
+
rescue LoadError
|
|
8
|
+
raise LoadError, "The `sassc` gem is required when developing the `better_errors` gem. "\
|
|
9
|
+
"If you're using a release of `better_errors`, the compiled CSS is missing from the released gem"
|
|
10
|
+
# If you arrived here because sassc is not in your project's Gemfile,
|
|
11
|
+
# the issue here is that the release of the better_errors gem
|
|
12
|
+
# is supposed to contain the compiled CSS, but that file is missing from the release.
|
|
13
|
+
# So better_errors is trying to build the CSS on the fly, which requires the sassc gem.
|
|
14
|
+
#
|
|
15
|
+
# If you're developing the better_errors gem locally, and you're running a project
|
|
16
|
+
# that does not have sassc in its bundle, run `rake style:build` in the better_errors
|
|
17
|
+
# project to compile the CSS file.
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
style_dir = File.expand_path("style", File.dirname(__FILE__))
|
|
21
|
+
style_file = "#{style_dir}/main.scss"
|
|
22
|
+
|
|
23
|
+
engine = SassC::Engine.new(
|
|
24
|
+
File.read(style_file),
|
|
25
|
+
filename: style_file,
|
|
26
|
+
style: for_deployment ? :compressed : :expanded,
|
|
27
|
+
line_comments: !for_deployment,
|
|
28
|
+
load_paths: [style_dir],
|
|
29
|
+
)
|
|
30
|
+
engine.render
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self.style_tag(csp_nonce)
|
|
34
|
+
style_file = File.expand_path("templates/main.css", File.dirname(__FILE__))
|
|
35
|
+
css = if File.exist?(style_file)
|
|
36
|
+
File.open(style_file).read
|
|
37
|
+
else
|
|
38
|
+
compiled_css(false)
|
|
39
|
+
end
|
|
40
|
+
"<style type='text/css' nonce='#{csp_nonce}'>\n#{css}\n</style>"
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module BetterErrors
|
|
2
|
+
class ExceptionHint
|
|
3
|
+
def initialize(exception)
|
|
4
|
+
@exception = exception
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def hint
|
|
8
|
+
case exception
|
|
9
|
+
when NoMethodError
|
|
10
|
+
/\Aundefined method `(?<method>[^']+)' for (?<val>[^:]+):(?<klass>\w+)/.match(exception.message) do |match|
|
|
11
|
+
if match[:val] == "nil"
|
|
12
|
+
return "Something is `nil` when it probably shouldn't be."
|
|
13
|
+
elsif !match[:klass].start_with? '0x'
|
|
14
|
+
return "`#{match[:method]}` is being called on a `#{match[:klass]}` object, "\
|
|
15
|
+
"which might not be the type of object you were expecting."
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
when NameError
|
|
19
|
+
/\Aundefined local variable or method `(?<method>[^']+)' for/.match(exception.message) do |match|
|
|
20
|
+
return "`#{match[:method]}` is probably misspelled."
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
attr_reader :exception
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
require "json"
|
|
2
2
|
require "ipaddr"
|
|
3
|
+
require "securerandom"
|
|
3
4
|
require "set"
|
|
4
5
|
require "rack"
|
|
5
6
|
|
|
@@ -39,6 +40,8 @@ module BetterErrors
|
|
|
39
40
|
allow_ip! "127.0.0.0/8"
|
|
40
41
|
allow_ip! "::1/128" rescue nil # windows ruby doesn't have ipv6 support
|
|
41
42
|
|
|
43
|
+
CSRF_TOKEN_COOKIE_NAME = "BetterErrors-#{BetterErrors::VERSION}-CSRF-Token"
|
|
44
|
+
|
|
42
45
|
# A new instance of BetterErrors::Middleware
|
|
43
46
|
#
|
|
44
47
|
# @param app The Rack app/middleware to wrap with Better Errors
|
|
@@ -72,7 +75,7 @@ module BetterErrors
|
|
|
72
75
|
def better_errors_call(env)
|
|
73
76
|
case env["PATH_INFO"]
|
|
74
77
|
when %r{/__better_errors/(?<id>.+?)/(?<method>\w+)\z}
|
|
75
|
-
internal_call
|
|
78
|
+
internal_call(env, $~[:id], $~[:method])
|
|
76
79
|
when %r{/__better_errors/?\z}
|
|
77
80
|
show_error_page env
|
|
78
81
|
else
|
|
@@ -89,11 +92,15 @@ module BetterErrors
|
|
|
89
92
|
end
|
|
90
93
|
|
|
91
94
|
def show_error_page(env, exception=nil)
|
|
95
|
+
request = Rack::Request.new(env)
|
|
96
|
+
csrf_token = request.cookies[CSRF_TOKEN_COOKIE_NAME] || SecureRandom.uuid
|
|
97
|
+
csp_nonce = SecureRandom.base64(12)
|
|
98
|
+
|
|
92
99
|
type, content = if @error_page
|
|
93
100
|
if text?(env)
|
|
94
|
-
[ 'plain', @error_page.
|
|
101
|
+
[ 'plain', @error_page.render_text ]
|
|
95
102
|
else
|
|
96
|
-
[ 'html', @error_page.
|
|
103
|
+
[ 'html', @error_page.render_main(csrf_token, csp_nonce) ]
|
|
97
104
|
end
|
|
98
105
|
else
|
|
99
106
|
[ 'html', no_errors_page ]
|
|
@@ -104,12 +111,37 @@ module BetterErrors
|
|
|
104
111
|
status_code = ActionDispatch::ExceptionWrapper.new(env, exception).status_code
|
|
105
112
|
end
|
|
106
113
|
|
|
107
|
-
|
|
114
|
+
headers = {
|
|
115
|
+
"Content-Type" => "text/#{type}; charset=utf-8",
|
|
116
|
+
"Content-Security-Policy" => [
|
|
117
|
+
"default-src 'none'",
|
|
118
|
+
# Specifying nonce makes a modern browser ignore 'unsafe-inline' which could still be set
|
|
119
|
+
# for older browsers without nonce support.
|
|
120
|
+
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src
|
|
121
|
+
"script-src 'self' 'nonce-#{csp_nonce}' 'unsafe-inline'",
|
|
122
|
+
"style-src 'self' 'nonce-#{csp_nonce}' 'unsafe-inline'",
|
|
123
|
+
"img-src data:",
|
|
124
|
+
"connect-src 'self'",
|
|
125
|
+
"navigate-to 'self' #{BetterErrors.editor.scheme}",
|
|
126
|
+
].join('; '),
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
response = Rack::Response.new(content, status_code, headers)
|
|
130
|
+
|
|
131
|
+
unless request.cookies[CSRF_TOKEN_COOKIE_NAME]
|
|
132
|
+
response.set_cookie(CSRF_TOKEN_COOKIE_NAME, value: csrf_token, path: "/", httponly: true, same_site: :strict)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# In older versions of Rack, the body returned here is actually a Rack::BodyProxy which seems to be a bug.
|
|
136
|
+
# (It contains status, headers and body and does not act like an array of strings.)
|
|
137
|
+
# Since we already have status code and body here, there's no need to use the ones in the Rack::Response.
|
|
138
|
+
(_status_code, headers, _body) = response.finish
|
|
139
|
+
[status_code, headers, [content]]
|
|
108
140
|
end
|
|
109
141
|
|
|
110
142
|
def text?(env)
|
|
111
143
|
env["HTTP_X_REQUESTED_WITH"] == "XMLHttpRequest" ||
|
|
112
|
-
|
|
144
|
+
!env["HTTP_ACCEPT"].to_s.include?('html')
|
|
113
145
|
end
|
|
114
146
|
|
|
115
147
|
def log_exception
|
|
@@ -129,13 +161,22 @@ module BetterErrors
|
|
|
129
161
|
end
|
|
130
162
|
end
|
|
131
163
|
|
|
132
|
-
def internal_call(env,
|
|
164
|
+
def internal_call(env, id, method)
|
|
165
|
+
return not_found_json_response unless %w[variables eval].include?(method)
|
|
133
166
|
return no_errors_json_response unless @error_page
|
|
134
|
-
return invalid_error_json_response if
|
|
167
|
+
return invalid_error_json_response if id != @error_page.id
|
|
168
|
+
|
|
169
|
+
request = Rack::Request.new(env)
|
|
170
|
+
return invalid_csrf_token_json_response unless request.cookies[CSRF_TOKEN_COOKIE_NAME]
|
|
171
|
+
|
|
172
|
+
request.body.rewind
|
|
173
|
+
body = JSON.parse(request.body.read)
|
|
174
|
+
return invalid_csrf_token_json_response unless request.cookies[CSRF_TOKEN_COOKIE_NAME] == body['csrfToken']
|
|
175
|
+
|
|
176
|
+
return not_acceptable_json_response unless request.content_type == 'application/json'
|
|
135
177
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
[200, { "Content-Type" => "text/plain; charset=utf-8" }, [JSON.dump(response)]]
|
|
178
|
+
response = @error_page.send("do_#{method}", body)
|
|
179
|
+
[200, { "Content-Type" => "application/json; charset=utf-8" }, [JSON.dump(response)]]
|
|
139
180
|
end
|
|
140
181
|
|
|
141
182
|
def no_errors_page
|
|
@@ -157,18 +198,40 @@ module BetterErrors
|
|
|
157
198
|
"The application has been restarted since this page loaded, " +
|
|
158
199
|
"or the framework is reloading all gems before each request "
|
|
159
200
|
end
|
|
160
|
-
[200, { "Content-Type" => "
|
|
201
|
+
[200, { "Content-Type" => "application/json; charset=utf-8" }, [JSON.dump(
|
|
161
202
|
error: 'No exception information available',
|
|
162
203
|
explanation: explanation,
|
|
163
204
|
)]]
|
|
164
205
|
end
|
|
165
206
|
|
|
166
207
|
def invalid_error_json_response
|
|
167
|
-
[200, { "Content-Type" => "
|
|
208
|
+
[200, { "Content-Type" => "application/json; charset=utf-8" }, [JSON.dump(
|
|
168
209
|
error: "Session expired",
|
|
169
210
|
explanation: "This page was likely opened from a previous exception, " +
|
|
170
211
|
"and the exception is no longer available in memory.",
|
|
171
212
|
)]]
|
|
172
213
|
end
|
|
214
|
+
|
|
215
|
+
def invalid_csrf_token_json_response
|
|
216
|
+
[200, { "Content-Type" => "application/json; charset=utf-8" }, [JSON.dump(
|
|
217
|
+
error: "Invalid CSRF Token",
|
|
218
|
+
explanation: "The browser session might have been cleared, " +
|
|
219
|
+
"or something went wrong.",
|
|
220
|
+
)]]
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def not_found_json_response
|
|
224
|
+
[404, { "Content-Type" => "application/json; charset=utf-8" }, [JSON.dump(
|
|
225
|
+
error: "Not found",
|
|
226
|
+
explanation: "Not a recognized internal call.",
|
|
227
|
+
)]]
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def not_acceptable_json_response
|
|
231
|
+
[406, { "Content-Type" => "application/json; charset=utf-8" }, [JSON.dump(
|
|
232
|
+
error: "Request not acceptable",
|
|
233
|
+
explanation: "The internal request did not match an acceptable content type.",
|
|
234
|
+
)]]
|
|
235
|
+
end
|
|
173
236
|
end
|
|
174
237
|
end
|
|
@@ -1,12 +1,23 @@
|
|
|
1
|
+
require 'better_errors/exception_hint'
|
|
2
|
+
|
|
1
3
|
# @private
|
|
2
4
|
module BetterErrors
|
|
3
5
|
class RaisedException
|
|
4
|
-
attr_reader :exception, :message, :backtrace
|
|
6
|
+
attr_reader :exception, :message, :backtrace, :hint
|
|
5
7
|
|
|
6
8
|
def initialize(exception)
|
|
7
|
-
if exception.respond_to?(:cause)
|
|
9
|
+
if exception.class.name == "ActionView::Template::Error" && exception.respond_to?(:cause)
|
|
10
|
+
# Rails 6+ exceptions of this type wrap the "real" exception, and the real exception
|
|
11
|
+
# is actually more useful than the ActionView-provided wrapper. Once Better Errors
|
|
12
|
+
# supports showing all exceptions in the cause stack, this should go away. Or perhaps
|
|
13
|
+
# this can be changed to provide guidance by showing the second error in the cause stack
|
|
14
|
+
# under this condition.
|
|
8
15
|
exception = exception.cause if exception.cause
|
|
9
16
|
elsif exception.respond_to?(:original_exception) && exception.original_exception
|
|
17
|
+
# This supports some specific Rails exceptions, and this is not intended to act the same as
|
|
18
|
+
# the Ruby's {Exception#cause}.
|
|
19
|
+
# It's possible this should only support ActionView::Template::Error, but by not changing
|
|
20
|
+
# this we're preserving longstanding behavior of Better Errors with Rails < 6.
|
|
10
21
|
exception = exception.original_exception
|
|
11
22
|
end
|
|
12
23
|
|
|
@@ -14,6 +25,7 @@ module BetterErrors
|
|
|
14
25
|
@message = exception.message
|
|
15
26
|
|
|
16
27
|
setup_backtrace
|
|
28
|
+
setup_hint
|
|
17
29
|
massage_syntax_error
|
|
18
30
|
end
|
|
19
31
|
|
|
@@ -36,8 +48,13 @@ module BetterErrors
|
|
|
36
48
|
|
|
37
49
|
def setup_backtrace_from_bindings
|
|
38
50
|
@backtrace = exception.__better_errors_bindings_stack.map { |binding|
|
|
39
|
-
|
|
40
|
-
|
|
51
|
+
if binding.respond_to?(:source_location) # Ruby >= 2.6
|
|
52
|
+
file = binding.source_location[0]
|
|
53
|
+
line = binding.source_location[1]
|
|
54
|
+
else
|
|
55
|
+
file = binding.eval "__FILE__"
|
|
56
|
+
line = binding.eval "__LINE__"
|
|
57
|
+
end
|
|
41
58
|
name = binding.frame_description
|
|
42
59
|
StackFrame.new(file, line, name, binding)
|
|
43
60
|
}
|
|
@@ -64,5 +81,9 @@ module BetterErrors
|
|
|
64
81
|
end
|
|
65
82
|
end
|
|
66
83
|
end
|
|
84
|
+
|
|
85
|
+
def setup_hint
|
|
86
|
+
@hint = ExceptionHint.new(exception).hint
|
|
87
|
+
end
|
|
67
88
|
end
|
|
68
89
|
end
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
*{margin:0;padding:0}table{width:100%;border-collapse:collapse}th,td{vertical-align:top;text-align:left}textarea{resize:none}body{font-size:10pt}body,td,input,textarea{font-family:helvetica neue, lucida grande, sans-serif;line-height:1.5;color:#333;text-shadow:0 1px 0 rgba(255,255,255,0.6)}html{background:#f0f0f5}.clearfix::after{clear:both;content:".";display:block;height:0;visibility:hidden}@media screen and (max-width: 1100px){html{overflow-y:scroll}body{margin:0 20px}header.exception{margin:0 -20px}nav.sidebar{padding:0;margin:20px 0}ul.frames{max-height:200px;overflow:auto}}@media screen and (min-width: 1100px){header.exception{position:fixed;top:0;left:0;right:0}nav.sidebar,.frame_info{position:fixed;top:102px;bottom:0;box-sizing:border-box;overflow-y:auto;overflow-x:hidden}nav.sidebar{width:40%;left:20px;top:122px;bottom:20px}.frame_info{display:none;right:0;left:40%;padding:20px;padding-left:10px;margin-left:30px}.frame_info.current{display:block}}nav.sidebar{background:#d3d3da;border-top:solid 3px #a33;border-bottom:solid 3px #a33;border-radius:4px;box-shadow:0 0 6px rgba(0,0,0,0.2),inset 0 0 0 1px rgba(0,0,0,0.1)}header.exception{padding:18px 20px;height:66px;min-height:59px;overflow:hidden;background-color:#20202a;color:#aaa;text-shadow:0 1px 0 rgba(0,0,0,0.3);font-weight:200;box-shadow:inset 0 -5px 3px -3px rgba(0,0,0,0.05),inset 0 -1px 0 rgba(0,0,0,0.05);-webkit-text-smoothing:antialiased}header.exception .fix-actions{margin-top:.5em}header.exception .fix-actions input[type=submit]{font-weight:bold}header.exception h2{font-weight:200;font-size:11pt}header.exception h2,header.exception p{line-height:1.5em;overflow:hidden;white-space:pre;text-overflow:ellipsis}header.exception h2 strong{font-weight:700;color:#d55}header.exception p{font-weight:200;font-size:17pt;color:white}header.exception:hover{height:auto;z-index:2}header.exception:hover h2,header.exception:hover p{padding-right:20px;overflow-y:auto;word-wrap:break-word;white-space:pre-wrap;height:auto;max-height:7.5em}@media screen and (max-width: 1100px){header.exception{height:auto}header.exception h2,header.exception p{padding-right:20px;overflow-y:auto;word-wrap:break-word;height:auto;max-height:7em}}.better-errors-javascript-not-loaded .backtrace .tabs{display:none}nav.tabs{border-bottom:solid 1px #ddd;background-color:#eee;text-align:center;padding:6px;box-shadow:inset 0 1px 0 rgba(255,255,255,0.1)}nav.tabs a{display:inline-block;height:22px;line-height:22px;padding:0 10px;text-decoration:none;font-size:8pt;font-weight:bold;color:#999;text-shadow:0 1px 0 rgba(255,255,255,0.6)}nav.tabs a.selected{color:white;background:rgba(0,0,0,0.5);border-radius:16px;box-shadow:1px 1px 0 rgba(255,255,255,0.1);text-shadow:0 0 4px rgba(0,0,0,0.4),0 1px 0 rgba(0,0,0,0.4)}nav.tabs a.disabled{text-decoration:line-through;text-shadow:none;cursor:default}ul.frames{box-shadow:0 0 10px rgba(0,0,0,0.1)}ul.frames li{background-color:#f8f8f8;background:-webkit-linear-gradient(top, #f8f8f8 80%, #f0f0f0);background:-moz-linear-gradient(top, #f8f8f8 80%, #f0f0f0);background:linear-gradient(top, #f8f8f8 80%, #f0f0f0);box-shadow:inset 0 -1px 0 #e2e2e2;padding:7px 20px;cursor:pointer;overflow:hidden}ul.frames .name,ul.frames .location{overflow:hidden;height:1.5em;white-space:nowrap;word-wrap:none;text-overflow:ellipsis}ul.frames .method{color:#966}ul.frames .location{font-size:0.85em;font-weight:400;color:#999}ul.frames .line{font-weight:bold}ul.frames li.selected{background:#38a;box-shadow:inset 0 1px 0 rgba(0,0,0,0.1),inset 0 2px 0 rgba(255,255,255,0.01),inset 0 -1px 0 rgba(0,0,0,0.1)}ul.frames li.selected .name,ul.frames li.selected .method,ul.frames li.selected .location{color:white;text-shadow:0 1px 0 rgba(0,0,0,0.2)}ul.frames li.selected .location{opacity:0.6}ul.frames li{padding-left:60px;position:relative}ul.frames li .icon{display:block;width:20px;height:20px;line-height:20px;border-radius:15px;text-align:center;background:white;border:solid 2px #ccc;font-size:9pt;font-weight:200;font-style:normal;position:absolute;top:14px;left:20px}ul.frames .icon.application{background:#808090;border-color:#555}ul.frames .icon.application:before{content:'A';color:white;text-shadow:0 0 3px rgba(0,0,0,0.2)}@media screen and (max-width: 1100px){ul.frames li{padding-top:6px;padding-bottom:6px;padding-left:36px;line-height:1.3}ul.frames li .icon{width:11px;height:11px;line-height:11px;top:7px;left:10px;font-size:5pt}ul.frames .name,ul.frames .location{display:inline-block;line-height:1.3;height:1.3em}ul.frames .name{margin-right:10px}}pre,code,.be-repl input,.be-repl .command-line span,textarea,.code_linenums{font-family:menlo, lucida console, monospace;font-size:8pt}p.no-javascript-notice{margin-bottom:1em;padding:1em;border:2px solid #e00}.better-errors-javascript-loaded .no-javascript-notice{display:none}.no-inline-style-notice{display:none}.trace_info{background:#fff;padding:6px;border-radius:3px;margin-bottom:2px;box-shadow:0 0 10px rgba(0,0,0,0.03),1px 1px 0 rgba(0,0,0,0.05),-1px 1px 0 rgba(0,0,0,0.05),0 0 0 4px rgba(0,0,0,0.04)}.code_block{background:#f1f1f1;border-left:1px solid #ccc}.trace_info .title{background:#f1f1f1;box-shadow:inset 0 1px 0 rgba(255,255,255,0.3);overflow:hidden;padding:6px 10px;border:solid 1px #ccc;border-bottom:0;border-top-left-radius:2px;border-top-right-radius:2px}.trace_info .title .name,.trace_info .title .location{font-size:9pt;line-height:26px;height:26px;overflow:hidden}.trace_info .title .location{float:left;font-weight:bold;font-size:10pt}.trace_info .title .location a{color:inherit;text-decoration:none;border-bottom:1px solid #aaaaaa}.trace_info .title .location a:hover{border-color:#666666}.trace_info .title .name{float:right;font-weight:200}.better-errors-javascript-not-loaded .be-repl{display:none}.code,.be-console,.unavailable{padding:5px;box-shadow:inset 3px 3px 3px rgba(0,0,0,0.1),inset 0 0 0 1px rgba(0,0,0,0.1)}.code,.unavailable{text-shadow:none}.code_linenums{background:#f1f1f1;padding-top:10px;padding-bottom:9px;float:left}.code_linenums span{display:block;padding:0 12px}.code,.be-console .syntax-highlighted{text-shadow:none}.code,.be-console .syntax-highlighted{background-color:#fdf6e3;color:#586e75}.code .c,.be-console .syntax-highlighted .c{color:#93a1a1}.code .err,.be-console .syntax-highlighted .err{color:#586e75}.code .g,.be-console .syntax-highlighted .g{color:#586e75}.code .k,.be-console .syntax-highlighted .k{color:#859900}.code .l,.be-console .syntax-highlighted .l{color:#586e75}.code .n,.be-console .syntax-highlighted .n{color:#586e75}.code .o,.be-console .syntax-highlighted .o{color:#859900}.code .x,.be-console .syntax-highlighted .x{color:#cb4b16}.code .p,.be-console .syntax-highlighted .p{color:#586e75}.code .cm,.be-console .syntax-highlighted .cm{color:#93a1a1}.code .cp,.be-console .syntax-highlighted .cp{color:#859900}.code .c1,.be-console .syntax-highlighted .c1{color:#93a1a1}.code .cs,.be-console .syntax-highlighted .cs{color:#859900}.code .gd,.be-console .syntax-highlighted .gd{color:#2aa198}.code .ge,.be-console .syntax-highlighted .ge{color:#586e75;font-style:italic}.code .gr,.be-console .syntax-highlighted .gr{color:#dc322f}.code .gh,.be-console .syntax-highlighted .gh{color:#cb4b16}.code .gi,.be-console .syntax-highlighted .gi{color:#859900}.code .go,.be-console .syntax-highlighted .go{color:#586e75}.code .gp,.be-console .syntax-highlighted .gp{color:#586e75}.code .gs,.be-console .syntax-highlighted .gs{color:#586e75;font-weight:bold}.code .gu,.be-console .syntax-highlighted .gu{color:#cb4b16}.code .gt,.be-console .syntax-highlighted .gt{color:#586e75}.code .kc,.be-console .syntax-highlighted .kc{color:#cb4b16}.code .kd,.be-console .syntax-highlighted .kd{color:#268bd2}.code .kn,.be-console .syntax-highlighted .kn{color:#859900}.code .kp,.be-console .syntax-highlighted .kp{color:#859900}.code .kr,.be-console .syntax-highlighted .kr{color:#268bd2}.code .kt,.be-console .syntax-highlighted .kt{color:#dc322f}.code .ld,.be-console .syntax-highlighted .ld{color:#586e75}.code .m,.be-console .syntax-highlighted .m{color:#2aa198}.code .s,.be-console .syntax-highlighted .s{color:#2aa198}.code .na,.be-console .syntax-highlighted .na{color:#586e75}.code .nb,.be-console .syntax-highlighted .nb{color:#B58900}.code .nc,.be-console .syntax-highlighted .nc{color:#268bd2}.code .no,.be-console .syntax-highlighted .no{color:#cb4b16}.code .nd,.be-console .syntax-highlighted .nd{color:#268bd2}.code .ni,.be-console .syntax-highlighted .ni{color:#cb4b16}.code .ne,.be-console .syntax-highlighted .ne{color:#cb4b16}.code .nf,.be-console .syntax-highlighted .nf{color:#268bd2}.code .nl,.be-console .syntax-highlighted .nl{color:#586e75}.code .nn,.be-console .syntax-highlighted .nn{color:#586e75}.code .nx,.be-console .syntax-highlighted .nx{color:#586e75}.code .py,.be-console .syntax-highlighted .py{color:#586e75}.code .nt,.be-console .syntax-highlighted .nt{color:#268bd2}.code .nv,.be-console .syntax-highlighted .nv{color:#268bd2}.code .ow,.be-console .syntax-highlighted .ow{color:#859900}.code .w,.be-console .syntax-highlighted .w{color:#586e75}.code .mf,.be-console .syntax-highlighted .mf{color:#2aa198}.code .mh,.be-console .syntax-highlighted .mh{color:#2aa198}.code .mi,.be-console .syntax-highlighted .mi{color:#2aa198}.code .mo,.be-console .syntax-highlighted .mo{color:#2aa198}.code .sb,.be-console .syntax-highlighted .sb{color:#93a1a1}.code .sc,.be-console .syntax-highlighted .sc{color:#2aa198}.code .sd,.be-console .syntax-highlighted .sd{color:#586e75}.code .s2,.be-console .syntax-highlighted .s2{color:#2aa198}.code .se,.be-console .syntax-highlighted .se{color:#cb4b16}.code .sh,.be-console .syntax-highlighted .sh{color:#586e75}.code .si,.be-console .syntax-highlighted .si{color:#2aa198}.code .sx,.be-console .syntax-highlighted .sx{color:#2aa198}.code .sr,.be-console .syntax-highlighted .sr{color:#dc322f}.code .s1,.be-console .syntax-highlighted .s1{color:#2aa198}.code .ss,.be-console .syntax-highlighted .ss{color:#2aa198}.code .bp,.be-console .syntax-highlighted .bp{color:#268bd2}.code .vc,.be-console .syntax-highlighted .vc{color:#268bd2}.code .vg,.be-console .syntax-highlighted .vg{color:#268bd2}.code .vi,.be-console .syntax-highlighted .vi{color:#268bd2}.code .il,.be-console .syntax-highlighted .il{color:#2aa198}@media (prefers-color-scheme: dark){.code{background-color:#002b36;color:#93a1a1}.code .c{color:#586e75;background-color:transparent;font-style:inherit}.code .err{color:#93a1a1;background-color:transparent;font-style:inherit}.code .g{color:#93a1a1;background-color:transparent;font-style:inherit}.code .k{color:#859900;background-color:transparent;font-style:inherit}.code .l{color:#93a1a1;background-color:transparent;font-style:inherit}.code .n{color:#93a1a1;background-color:transparent;font-style:inherit}.code .o{color:#859900;background-color:transparent;font-style:inherit}.code .x{color:#cb4b16;background-color:transparent;font-style:inherit}.code .p{color:#93a1a1;background-color:transparent;font-style:inherit}.code .cm{color:#586e75;background-color:transparent;font-style:inherit}.code .cp{color:#859900;background-color:transparent;font-style:inherit}.code .c1{color:#586e75;background-color:transparent;font-style:inherit}.code .cs{color:#859900;background-color:transparent;font-style:inherit}.code .gd{color:#2aa198;background-color:transparent;font-style:inherit}.code .ge{color:#93a1a1;background-color:transparent;font-style:italic}.code .gr{color:#dc322f;background-color:transparent;font-style:inherit}.code .gh{color:#cb4b16;background-color:transparent;font-style:inherit}.code .gi{color:#859900;background-color:transparent;font-style:inherit}.code .go{color:#93a1a1;background-color:transparent;font-style:inherit}.code .gp{color:#93a1a1;background-color:transparent;font-style:inherit}.code .gs{color:#93a1a1;background-color:transparent;font-weight:bold}.code .gu{color:#cb4b16;background-color:transparent;font-style:inherit}.code .gt{color:#93a1a1;background-color:transparent;font-style:inherit}.code .kc{color:#cb4b16;background-color:transparent;font-style:inherit}.code .kd{color:#268bd2;background-color:transparent;font-style:inherit}.code .kn{color:#859900;background-color:transparent;font-style:inherit}.code .kp{color:#859900;background-color:transparent;font-style:inherit}.code .kr{color:#268bd2;background-color:transparent;font-style:inherit}.code .kt{color:#dc322f;background-color:transparent;font-style:inherit}.code .ld{color:#93a1a1;background-color:transparent;font-style:inherit}.code .m{color:#2aa198;background-color:transparent;font-style:inherit}.code .s{color:#2aa198;background-color:transparent;font-style:inherit}.code .na{color:#93a1a1;background-color:transparent;font-style:inherit}.code .nb{color:#B58900;background-color:transparent;font-style:inherit}.code .nc{color:#268bd2;background-color:transparent;font-style:inherit}.code .no{color:#cb4b16;background-color:transparent;font-style:inherit}.code .nd{color:#268bd2;background-color:transparent;font-style:inherit}.code .ni{color:#cb4b16;background-color:transparent;font-style:inherit}.code .ne{color:#cb4b16;background-color:transparent;font-style:inherit}.code .nf{color:#268bd2;background-color:transparent;font-style:inherit}.code .nl{color:#93a1a1;background-color:transparent;font-style:inherit}.code .nn{color:#93a1a1;background-color:transparent;font-style:inherit}.code .nx{color:#93a1a1;background-color:transparent;font-style:inherit}.code .py{color:#93a1a1;background-color:transparent;font-style:inherit}.code .nt{color:#268bd2;background-color:transparent;font-style:inherit}.code .nv{color:#268bd2;background-color:transparent;font-style:inherit}.code .ow{color:#859900;background-color:transparent;font-style:inherit}.code .w{color:#93a1a1;background-color:transparent;font-style:inherit}.code .mf{color:#2aa198;background-color:transparent;font-style:inherit}.code .mh{color:#2aa198;background-color:transparent;font-style:inherit}.code .mi{color:#2aa198;background-color:transparent;font-style:inherit}.code .mo{color:#2aa198;background-color:transparent;font-style:inherit}.code .sb{color:#586e75;background-color:transparent;font-style:inherit}.code .sc{color:#2aa198;background-color:transparent;font-style:inherit}.code .sd{color:#93a1a1;background-color:transparent;font-style:inherit}.code .s2{color:#2aa198;background-color:transparent;font-style:inherit}.code .se{color:#cb4b16;background-color:transparent;font-style:inherit}.code .sh{color:#93a1a1;background-color:transparent;font-style:inherit}.code .si{color:#2aa198;background-color:transparent;font-style:inherit}.code .sx{color:#2aa198;background-color:transparent;font-style:inherit}.code .sr{color:#dc322f;background-color:transparent;font-style:inherit}.code .s1{color:#2aa198;background-color:transparent;font-style:inherit}.code .ss{color:#2aa198;background-color:transparent;font-style:inherit}.code .bp{color:#268bd2;background-color:transparent;font-style:inherit}.code .vc{color:#268bd2;background-color:transparent;font-style:inherit}.code .vg{color:#268bd2;background-color:transparent;font-style:inherit}.code .vi{color:#268bd2;background-color:transparent;font-style:inherit}.code .il{color:#2aa198;background-color:transparent;font-style:inherit}}.code{margin-bottom:-1px;border-top-left-radius:2px;padding:10px 0;overflow:auto}.code .code-wrapper{display:inline-block;min-width:100%}.code pre{padding-left:12px;min-height:16px}p.unavailable{padding:20px 0 40px 0;text-align:center;color:#b99;font-weight:bold}p.unavailable:before{content:'\00d7';display:block;color:#daa;text-align:center;font-size:40pt;font-weight:normal;margin-bottom:-10px}@-webkit-keyframes highlight{0%{background:rgba(51,136,170,0.45)}100%{background:rgba(51,136,170,0.15)}}@-moz-keyframes highlight{0%{background:rgba(51,136,170,0.45)}100%{background:rgba(51,136,170,0.15)}}@keyframes highlight{0%{background:rgba(51,136,170,0.45)}100%{background:rgba(51,136,170,0.15)}}.code .highlight,.code_linenums .highlight{background:rgba(51,136,170,0.15);-webkit-animation:highlight 400ms linear 1;-moz-animation:highlight 400ms linear 1;animation:highlight 400ms linear 1}.be-console{background:#fff;padding:0 1px 10px 1px;border-bottom-left-radius:2px;border-bottom-right-radius:2px}.be-console pre{padding:10px 10px 0 10px;max-height:400px;overflow-x:none;overflow-y:auto;margin-bottom:-3px;word-wrap:break-word;white-space:pre-wrap}.be-console .command-line{display:table;width:100%}.be-console .command-line span,.be-console .command-line input{display:table-cell}.be-console .command-line span{width:1%;padding-right:5px;padding-left:10px;white-space:pre}.be-console .command-line input{width:99%}.be-console input,.be-console input:focus{outline:0;border:0;padding:0;background:transparent;margin:0}.hint{margin:15px 0 20px 0;font-size:8pt;color:#8080a0;padding-left:20px}.console-has-been-used .live-console-hint{display:none}.better-errors-javascript-not-loaded .live-console-hint{display:none}.hint:before{content:'\25b2';margin-right:5px;opacity:0.5}.sub{padding:10px 0;margin:10px 0}.sub h3{color:#39a;font-size:1.1em;margin:10px 0;text-shadow:0 1px 0 rgba(255,255,255,0.6);-webkit-font-smoothing:antialiased}.sub .inset{overflow-y:auto}.sub table{table-layout:fixed}.sub table td{border-top:dotted 1px #ddd;padding:7px 1px}.sub table td.name{width:150px;font-weight:bold;font-size:0.8em;padding-right:20px;word-wrap:break-word}.sub table td pre{max-height:15em;overflow-y:auto}.sub table td pre{width:100%;word-wrap:break-word;white-space:normal}.sub .unsupported{font-family:sans-serif;color:#777}nav.sidebar::-webkit-scrollbar,.inset pre::-webkit-scrollbar,.be-console pre::-webkit-scrollbar,.code::-webkit-scrollbar{width:10px;height:10px}.inset pre::-webkit-scrollbar-thumb,.be-console pre::-webkit-scrollbar-thumb,.code::-webkit-scrollbar-thumb{background:#ccc;border-radius:5px}nav.sidebar::-webkit-scrollbar-thumb{background:rgba(0,0,0,0);border-radius:5px}nav.sidebar:hover::-webkit-scrollbar-thumb{background-color:#999;background:-webkit-linear-gradient(left, #aaa, #999)}.be-console pre:hover::-webkit-scrollbar-thumb,.inset pre:hover::-webkit-scrollbar-thumb,.code:hover::-webkit-scrollbar-thumb{background:#888}
|