diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml
new file mode 100644
index 00000000..75d0072a
--- /dev/null
+++ b/.github/release-drafter.yml
@@ -0,0 +1,46 @@
+---
+name-template: 'v$RESOLVED_VERSION'
+tag-template: 'v$RESOLVED_VERSION'
+autolabeler:
+ - label: breaking change
+ title:
+ - '/(? :test
@@ -36,3 +37,18 @@ namespace :test do
with_each_gemfile { sh "bundle exec rspec" rescue nil }
end
end
+
+namespace :style do
+ desc "Build main.css from the SASS sources"
+ task :build do
+ css = BetterErrors::ErrorPageStyle.compiled_css(true)
+ File.open(File.expand_path("lib/better_errors/templates/main.css", File.dirname(__FILE__)), "w") do |f|
+ f.write(css)
+ end
+ end
+
+ desc "Remove main.css so that the SASS sources will be used directly"
+ task :develop do
+ File.unlink File.expand_path("lib/better_errors/templates/main.css", File.dirname(__FILE__))
+ end
+end
diff --git a/better_errors.gemspec b/better_errors.gemspec
index ef996b0d..04f74331 100644
--- a/better_errors.gemspec
+++ b/better_errors.gemspec
@@ -5,16 +5,16 @@ require 'better_errors/version'
Gem::Specification.new do |s|
s.name = "better_errors"
s.version = BetterErrors::VERSION
- s.authors = ["Charlie Somerville"]
- s.email = ["charlie@charliesomerville.com"]
+ s.authors = ["Hailey Somerville"]
+ s.email = ["hailey@hailey.lol"]
s.description = %q{Provides a better error page for Rails and other Rack apps. Includes source code inspection, a live REPL and local/instance variable inspection for all stack frames.}
s.summary = %q{Better error page for Rails and other Rack apps}
s.homepage = "https://github.com/BetterErrors/better_errors"
s.license = "MIT"
- s.files = `git ls-files -z`.split("\x0").reject do |f|
- f.match(%r{^((test|spec|features|feature-screenshots)/|Rakefile)})
- end
+ s.files = `git ls-files -z`.split("\x0").reject { |f|
+ f.match(%r{^((test|spec|features|feature-screenshots)/|Rakefile)|\.scss$})
+ } + %w[lib/better_errors/templates/main.css]
s.require_paths = ["lib"]
@@ -25,12 +25,13 @@ Gem::Specification.new do |s|
s.add_development_dependency "rspec-html-matchers"
s.add_development_dependency "rspec-its"
s.add_development_dependency "yard"
+ s.add_development_dependency "sassc"
# kramdown 2.1 requires Ruby 2.3+
s.add_development_dependency "kramdown", (RUBY_VERSION < '2.3' ? '< 2.0.0' : '> 2.0.0')
# simplecov and coveralls must not be included here. See the Gemfiles instead.
s.add_dependency "erubi", ">= 1.0.0"
- s.add_dependency "coderay", ">= 1.0.0"
+ s.add_dependency "rouge", ">= 1.0.0"
s.add_dependency "rack", ">= 0.9.0"
# optional dependencies:
diff --git a/gemfiles/pry010.gemfile b/gemfiles/pry010.gemfile
index 4503b67c..1daca580 100644
--- a/gemfiles/pry010.gemfile
+++ b/gemfiles/pry010.gemfile
@@ -4,6 +4,7 @@ gem 'rack', RUBY_VERSION < '2.2.2' ? '~> 1.6' : '~> 2.0'
gem "binding_of_caller"
gem "pry", "~> 0.10.0"
-gem 'coveralls', require: false
+gem 'simplecov', require: false
+gem 'simplecov-lcov', require: false
gemspec path: "../"
diff --git a/gemfiles/pry011.gemfile b/gemfiles/pry011.gemfile
index 761c5918..206b89d7 100644
--- a/gemfiles/pry011.gemfile
+++ b/gemfiles/pry011.gemfile
@@ -3,6 +3,7 @@ source "https://rubygems.org"
gem 'rack', RUBY_VERSION < '2.2.2' ? '~> 1.6' : '~> 2.0'
gem "pry", "~> 0.11.0pre"
-gem 'coveralls', require: false
+gem 'simplecov', require: false
+gem 'simplecov-lcov', require: false
gemspec path: "../"
diff --git a/gemfiles/pry09.gemfile b/gemfiles/pry09.gemfile
index db7fa6bc..130326b9 100644
--- a/gemfiles/pry09.gemfile
+++ b/gemfiles/pry09.gemfile
@@ -3,6 +3,7 @@ source "https://rubygems.org"
gem 'rack', RUBY_VERSION < '2.2.2' ? '~> 1.6' : '~> 2.0'
gem "pry", "~> 0.9.12"
-gem 'coveralls', require: false
+gem 'simplecov', require: false
+gem 'simplecov-lcov', require: false
gemspec path: "../"
diff --git a/gemfiles/rack.gemfile b/gemfiles/rack.gemfile
index 4f2bab73..b5bbff7a 100644
--- a/gemfiles/rack.gemfile
+++ b/gemfiles/rack.gemfile
@@ -2,6 +2,7 @@ source "https://rubygems.org"
gem 'rack', RUBY_VERSION < '2.2.2' ? '~> 1.6' : '~> 2.0'
-gem 'coveralls', require: false
+gem 'simplecov', require: false
+gem 'simplecov-lcov', require: false
gemspec path: "../"
diff --git a/gemfiles/rack_boc.gemfile b/gemfiles/rack_boc.gemfile
index 0899c028..09520d9b 100644
--- a/gemfiles/rack_boc.gemfile
+++ b/gemfiles/rack_boc.gemfile
@@ -3,6 +3,7 @@ source "https://rubygems.org"
gem 'rack', RUBY_VERSION < '2.2.2' ? '~> 1.6' : '~> 2.0'
gem "binding_of_caller"
-gem 'coveralls', require: false
+gem 'simplecov', require: false
+gem 'simplecov-lcov', require: false
gemspec path: "../"
diff --git a/gemfiles/rails42.gemfile b/gemfiles/rails42.gemfile
index ba2e191f..caaeb8ef 100644
--- a/gemfiles/rails42.gemfile
+++ b/gemfiles/rails42.gemfile
@@ -4,6 +4,7 @@ gem "rails", "~> 4.2.0"
gem 'nokogiri', RUBY_VERSION < '2.1' ? '~> 1.6.0' : '>= 1.7'
gem 'i18n', '< 1.5.2' if RUBY_VERSION < '2.3'
-gem 'coveralls', require: false
+gem 'simplecov', require: false
+gem 'simplecov-lcov', require: false
gemspec path: "../"
diff --git a/gemfiles/rails42_boc.gemfile b/gemfiles/rails42_boc.gemfile
index 2e66ac54..c3619a6a 100644
--- a/gemfiles/rails42_boc.gemfile
+++ b/gemfiles/rails42_boc.gemfile
@@ -5,6 +5,7 @@ gem 'nokogiri', RUBY_VERSION < '2.1' ? '~> 1.6.0' : '>= 1.7'
gem 'i18n', '< 1.5.2' if RUBY_VERSION < '2.3'
gem "binding_of_caller"
-gem 'coveralls', require: false
+gem 'simplecov', require: false
+gem 'simplecov-lcov', require: false
gemspec path: "../"
diff --git a/gemfiles/rails42_haml.gemfile b/gemfiles/rails42_haml.gemfile
index a22370a6..6f656aae 100644
--- a/gemfiles/rails42_haml.gemfile
+++ b/gemfiles/rails42_haml.gemfile
@@ -5,6 +5,7 @@ gem 'nokogiri', RUBY_VERSION < '2.1' ? '~> 1.6.0' : '>= 1.7'
gem 'i18n', '< 1.5.2' if RUBY_VERSION < '2.3'
gem "haml"
-gem 'coveralls', require: false
+gem 'simplecov', require: false
+gem 'simplecov-lcov', require: false
gemspec path: "../"
diff --git a/gemfiles/rails50.gemfile b/gemfiles/rails50.gemfile
index 7d31f08d..856109f5 100644
--- a/gemfiles/rails50.gemfile
+++ b/gemfiles/rails50.gemfile
@@ -3,6 +3,7 @@ source "https://rubygems.org"
gem "rails", "~> 5.0.0"
gem 'i18n', '< 1.5.2' if RUBY_VERSION < '2.3'
-gem 'coveralls', require: false
+gem 'simplecov', require: false
+gem 'simplecov-lcov', require: false
gemspec path: "../"
diff --git a/gemfiles/rails50_boc.gemfile b/gemfiles/rails50_boc.gemfile
index a7955aad..6632ab09 100644
--- a/gemfiles/rails50_boc.gemfile
+++ b/gemfiles/rails50_boc.gemfile
@@ -4,6 +4,7 @@ gem "rails", "~> 5.0.0"
gem 'i18n', '< 1.5.2' if RUBY_VERSION < '2.3'
gem "binding_of_caller"
-gem 'coveralls', require: false
+gem 'simplecov', require: false
+gem 'simplecov-lcov', require: false
gemspec path: "../"
diff --git a/gemfiles/rails50_haml.gemfile b/gemfiles/rails50_haml.gemfile
index 5ad597ec..c549086b 100644
--- a/gemfiles/rails50_haml.gemfile
+++ b/gemfiles/rails50_haml.gemfile
@@ -4,6 +4,7 @@ gem "rails", "~> 5.0.0"
gem 'i18n', '< 1.5.2' if RUBY_VERSION < '2.3'
gem "haml"
-gem 'coveralls', require: false
+gem 'simplecov', require: false
+gem 'simplecov-lcov', require: false
gemspec path: "../"
diff --git a/gemfiles/rails51.gemfile b/gemfiles/rails51.gemfile
index 5532febb..6b80d819 100644
--- a/gemfiles/rails51.gemfile
+++ b/gemfiles/rails51.gemfile
@@ -3,6 +3,7 @@ source "https://rubygems.org"
gem "rails", "~> 5.1.0"
gem 'i18n', '< 1.5.2', require: false if RUBY_VERSION < '2.3'
-gem 'coveralls', require: false
+gem 'simplecov', require: false
+gem 'simplecov-lcov', require: false
gemspec path: "../"
diff --git a/gemfiles/rails51_boc.gemfile b/gemfiles/rails51_boc.gemfile
index bfb2cc92..391459d4 100644
--- a/gemfiles/rails51_boc.gemfile
+++ b/gemfiles/rails51_boc.gemfile
@@ -4,6 +4,7 @@ gem "rails", "~> 5.1.0"
gem 'i18n', '< 1.5.2' if RUBY_VERSION < '2.3'
gem "binding_of_caller"
-gem 'coveralls', require: false
+gem 'simplecov', require: false
+gem 'simplecov-lcov', require: false
gemspec path: "../"
diff --git a/gemfiles/rails51_haml.gemfile b/gemfiles/rails51_haml.gemfile
index a870a7cf..4be7c4bc 100644
--- a/gemfiles/rails51_haml.gemfile
+++ b/gemfiles/rails51_haml.gemfile
@@ -4,6 +4,7 @@ gem "rails", "~> 5.1.0"
gem 'i18n', '< 1.5.2' if RUBY_VERSION < '2.3'
gem "haml"
-gem 'coveralls', require: false
+gem 'simplecov', require: false
+gem 'simplecov-lcov', require: false
gemspec path: "../"
diff --git a/gemfiles/rails52.gemfile b/gemfiles/rails52.gemfile
index 070970cd..887de07d 100644
--- a/gemfiles/rails52.gemfile
+++ b/gemfiles/rails52.gemfile
@@ -3,6 +3,7 @@ source "https://rubygems.org"
gem "rails", "~> 5.2.0"
gem 'i18n', '< 1.5.2' if RUBY_VERSION < '2.3'
-gem 'coveralls', require: false
+gem 'simplecov', require: false
+gem 'simplecov-lcov', require: false
gemspec path: "../"
diff --git a/gemfiles/rails52_boc.gemfile b/gemfiles/rails52_boc.gemfile
index 195e4e23..669c73fd 100644
--- a/gemfiles/rails52_boc.gemfile
+++ b/gemfiles/rails52_boc.gemfile
@@ -4,6 +4,7 @@ gem "rails", "~> 5.2.0"
gem 'i18n', '< 1.5.2' if RUBY_VERSION < '2.3'
gem "binding_of_caller"
-gem 'coveralls', require: false
+gem 'simplecov', require: false
+gem 'simplecov-lcov', require: false
gemspec path: "../"
diff --git a/gemfiles/rails52_haml.gemfile b/gemfiles/rails52_haml.gemfile
index dd9f0dc9..c2bb1f57 100644
--- a/gemfiles/rails52_haml.gemfile
+++ b/gemfiles/rails52_haml.gemfile
@@ -4,6 +4,7 @@ gem "rails", "~> 5.2.0"
gem 'i18n', '< 1.5.2' if RUBY_VERSION < '2.3'
gem "haml"
-gem 'coveralls', require: false
+gem 'simplecov', require: false
+gem 'simplecov-lcov', require: false
gemspec path: "../"
diff --git a/gemfiles/rails60.gemfile b/gemfiles/rails60.gemfile
index 55f89c5e..825f46ca 100644
--- a/gemfiles/rails60.gemfile
+++ b/gemfiles/rails60.gemfile
@@ -2,6 +2,7 @@ source "https://rubygems.org"
gem "rails", "~> 6.0.0"
-gem 'coveralls', require: false
+gem 'simplecov', require: false
+gem 'simplecov-lcov', require: false
gemspec path: "../"
diff --git a/gemfiles/rails60_boc.gemfile b/gemfiles/rails60_boc.gemfile
index 190c573c..c32cb5d9 100644
--- a/gemfiles/rails60_boc.gemfile
+++ b/gemfiles/rails60_boc.gemfile
@@ -3,6 +3,7 @@ source "https://rubygems.org"
gem "rails", "~> 6.0.0"
gem "binding_of_caller"
-gem 'coveralls', require: false
+gem 'simplecov', require: false
+gem 'simplecov-lcov', require: false
gemspec path: "../"
diff --git a/gemfiles/rails60_haml.gemfile b/gemfiles/rails60_haml.gemfile
index 84d3401a..e4f3179c 100644
--- a/gemfiles/rails60_haml.gemfile
+++ b/gemfiles/rails60_haml.gemfile
@@ -3,6 +3,7 @@ source "https://rubygems.org"
gem "rails", "~> 6.0.0"
gem "haml"
-gem 'coveralls', require: false
+gem 'simplecov', require: false
+gem 'simplecov-lcov', require: false
gemspec path: "../"
diff --git a/gemfiles/rails61.gemfile b/gemfiles/rails61.gemfile
new file mode 100644
index 00000000..d185530c
--- /dev/null
+++ b/gemfiles/rails61.gemfile
@@ -0,0 +1,8 @@
+source "https://rubygems.org"
+
+gem "rails", "~> 6.1.0rc"
+
+gem 'simplecov', require: false
+gem 'simplecov-lcov', require: false
+
+gemspec path: "../"
diff --git a/gemfiles/rails61_boc.gemfile b/gemfiles/rails61_boc.gemfile
new file mode 100644
index 00000000..50442047
--- /dev/null
+++ b/gemfiles/rails61_boc.gemfile
@@ -0,0 +1,9 @@
+source "https://rubygems.org"
+
+gem "rails", "~> 6.1.0rc"
+gem "binding_of_caller"
+
+gem 'simplecov', require: false
+gem 'simplecov-lcov', require: false
+
+gemspec path: "../"
diff --git a/gemfiles/rails61_haml.gemfile b/gemfiles/rails61_haml.gemfile
new file mode 100644
index 00000000..6685996f
--- /dev/null
+++ b/gemfiles/rails61_haml.gemfile
@@ -0,0 +1,9 @@
+source "https://rubygems.org"
+
+gem "rails", "~> 6.1.0rc"
+gem "haml"
+
+gem 'simplecov', require: false
+gem 'simplecov-lcov', require: false
+
+gemspec path: "../"
diff --git a/lib/better_errors.rb b/lib/better_errors.rb
index ee7a48bd..aff646e1 100644
--- a/lib/better_errors.rb
+++ b/lib/better_errors.rb
@@ -1,8 +1,8 @@
require "pp"
require "erubi"
-require "coderay"
require "uri"
+require "better_errors/version"
require "better_errors/code_formatter"
require "better_errors/inspectable_value"
require "better_errors/error_page"
@@ -10,21 +10,9 @@
require "better_errors/raised_exception"
require "better_errors/repl"
require "better_errors/stack_frame"
-require "better_errors/version"
+require "better_errors/editor"
module BetterErrors
- POSSIBLE_EDITOR_PRESETS = [
- { symbols: [:emacs, :emacsclient], sniff: /emacs/i, url: "emacs://open?url=file://%{file}&line=%{line}" },
- { symbols: [:macvim, :mvim], sniff: /vim/i, url: proc { |file, line| "mvim://open?url=file://#{file}&line=#{line}" } },
- { symbols: [:sublime, :subl, :st], sniff: /subl/i, url: "subl://open?url=file://%{file}&line=%{line}" },
- { symbols: [:textmate, :txmt, :tm], sniff: /mate/i, url: "txmt://open?url=file://%{file}&line=%{line}" },
- { symbols: [:idea], sniff: /idea/i, url: "idea://open?file=%{file}&line=%{line}" },
- { symbols: [:rubymine], sniff: /mine/i, url: "x-mine://open?file=%{file}&line=%{line}" },
- { symbols: [:vscode, :code], sniff: /code/i, url: "vscode://file/%{file}:%{line}" },
- { symbols: [:vscodium, :codium], sniff: /codium/i, url: "vscodium://file/%{file}:%{line}" },
- { symbols: [:atom], sniff: /atom/i, url: "atom://core/open/file?filename=%{file}&line=%{line}" },
- ]
-
class << self
# The path to the root of the application. Better Errors uses this property
# to determine if a file in a backtrace should be considered an application
@@ -64,17 +52,18 @@ class << self
@maximum_variable_inspect_size = 100_000
@ignored_classes = ['ActionDispatch::Request', 'ActionDispatch::Response']
- # Returns a proc, which when called with a filename and line number argument,
+ # Returns an object which responds to #url, which when called with
+ # a filename and line number argument,
# returns a URL to open the filename and line in the selected editor.
#
# Generates TextMate URLs by default.
#
- # BetterErrors.editor["/some/file", 123]
+ # BetterErrors.editor.url("/some/file", 123)
# # => txmt://open?url=file:///some/file&line=123
#
# @return [Proc]
def self.editor
- @editor
+ @editor ||= default_editor
end
# Configures how Better Errors generates open-in-editor URLs.
@@ -115,20 +104,15 @@ def self.editor
# @param [Proc] proc
#
def self.editor=(editor)
- POSSIBLE_EDITOR_PRESETS.each do |config|
- if config[:symbols].include?(editor)
- return self.editor = config[:url]
- end
- end
-
- if editor.is_a? String
- self.editor = proc { |file, line| editor % { file: URI.encode_www_form_component(file), line: line } }
+ if editor.is_a? Symbol
+ @editor = Editor.editor_from_symbol(editor)
+ raise(ArgumentError, "Symbol #{editor} is not a symbol in the list of supported errors.") unless editor
+ elsif editor.is_a? String
+ @editor = Editor.for_formatting_string(editor)
+ elsif editor.respond_to? :call
+ @editor = Editor.for_proc(editor)
else
- if editor.respond_to? :call
- @editor = editor
- else
- raise TypeError, "Expected editor to be a valid editor key, a format string or a callable."
- end
+ raise ArgumentError, "Expected editor to be a valid editor key, a format string or a callable."
end
end
@@ -145,12 +129,8 @@ def self.use_pry!
#
# @return [Symbol]
def self.default_editor
- POSSIBLE_EDITOR_PRESETS.detect(-> { {} }) { |config|
- ENV["EDITOR"] =~ config[:sniff]
- }[:url] || :textmate
+ Editor.default_editor
end
-
- BetterErrors.editor = default_editor
end
begin
diff --git a/lib/better_errors/code_formatter.rb b/lib/better_errors/code_formatter.rb
index 4c6824d1..29092af9 100644
--- a/lib/better_errors/code_formatter.rb
+++ b/lib/better_errors/code_formatter.rb
@@ -4,14 +4,6 @@ class CodeFormatter
require "better_errors/code_formatter/html"
require "better_errors/code_formatter/text"
- FILE_TYPES = {
- ".rb" => :ruby,
- "" => :ruby,
- ".html" => :html,
- ".erb" => :erb,
- ".haml" => :haml
- }
-
attr_reader :filename, :line, :context
def initialize(filename, line, context = 5)
@@ -26,13 +18,21 @@ def output
source_unavailable
end
- def formatted_code
- formatted_lines.join
+ def line_range
+ min = [line - context, 1].max
+ max = [line + context, source_lines.count].min
+ min..max
end
- def coderay_scanner
- ext = File.extname(filename)
- FILE_TYPES[ext] || :text
+ def context_lines
+ range = line_range
+ source_lines[(range.begin - 1)..(range.end - 1)] or raise Errno::EINVAL
+ end
+
+ private
+
+ def formatted_code
+ formatted_lines.join
end
def each_line_of(lines, &blk)
@@ -41,23 +41,12 @@ def each_line_of(lines, &blk)
}
end
- def highlighted_lines
- CodeRay.scan(context_lines.join, coderay_scanner).div(wrap: nil).lines
- end
-
- def context_lines
- range = line_range
- source_lines[(range.begin - 1)..(range.end - 1)] or raise Errno::EINVAL
+ def source
+ @source ||= File.read(filename)
end
def source_lines
- @source_lines ||= File.readlines(filename)
- end
-
- def line_range
- min = [line - context, 1].max
- max = [line + context, source_lines.count].min
- min..max
+ @source_lines ||= source.lines
end
end
end
diff --git a/lib/better_errors/code_formatter/html.rb b/lib/better_errors/code_formatter/html.rb
index ec96a214..3e94cd4b 100644
--- a/lib/better_errors/code_formatter/html.rb
+++ b/lib/better_errors/code_formatter/html.rb
@@ -1,3 +1,5 @@
+require "rouge"
+
module BetterErrors
# @private
class CodeFormatter::HTML < CodeFormatter
@@ -20,7 +22,19 @@ def formatted_nums
end
def formatted_code
- %{
#{formatted_nums.join}
#{super}
}
+ %{
+ #{formatted_nums.join}
+
+ }
+ end
+
+ def rouge_lexer
+ Rouge::Lexer.guess(filename: filename, source: source) { Rouge::Lexers::Ruby }
end
+
+ def highlighted_lines
+ Rouge::Formatters::HTML.new.format(rouge_lexer.lex(context_lines.join)).lines
+ end
+
end
end
diff --git a/lib/better_errors/editor.rb b/lib/better_errors/editor.rb
new file mode 100644
index 00000000..96ffc3f0
--- /dev/null
+++ b/lib/better_errors/editor.rb
@@ -0,0 +1,103 @@
+require "uri"
+
+module BetterErrors
+ class Editor
+ KNOWN_EDITORS = [
+ { symbols: [:atom], sniff: /atom/i, url: "atom://core/open/file?filename=%{file}&line=%{line}" },
+ { symbols: [:emacs, :emacsclient], sniff: /emacs/i, url: "emacs://open?url=file://%{file}&line=%{line}" },
+ { symbols: [:idea], sniff: /idea/i, url: "idea://open?file=%{file}&line=%{line}" },
+ { symbols: [:macvim, :mvim], sniff: /vim/i, url: "mvim://open?url=file://%{file_unencoded}&line=%{line}" },
+ { symbols: [:rubymine], sniff: /mine/i, url: "x-mine://open?file=%{file}&line=%{line}" },
+ { symbols: [:sublime, :subl, :st], sniff: /subl/i, url: "subl://open?url=file://%{file}&line=%{line}" },
+ { symbols: [:textmate, :txmt, :tm], sniff: /mate/i, url: "txmt://open?url=file://%{file}&line=%{line}" },
+ { symbols: [:vscode, :code], sniff: /code/i, url: "vscode://file/%{file}:%{line}" },
+ { symbols: [:vscodium, :codium], sniff: /codium/i, url: "vscodium://file/%{file}:%{line}" },
+ ]
+
+ def self.for_formatting_string(formatting_string)
+ new proc { |file, line|
+ formatting_string % { file: URI.encode_www_form_component(file), file_unencoded: file, line: line }
+ }
+ end
+
+ def self.for_proc(url_proc)
+ new url_proc
+ end
+
+ # Automatically sniffs a default editor preset based on
+ # environment variables.
+ #
+ # @return [Symbol]
+ def self.default_editor
+ editor_from_environment_formatting_string ||
+ editor_from_environment_editor ||
+ editor_from_symbol(:textmate)
+ end
+
+ def self.editor_from_environment_editor
+ if ENV["BETTER_ERRORS_EDITOR"]
+ editor = editor_from_command(ENV["BETTER_ERRORS_EDITOR"])
+ return editor if editor
+ puts "BETTER_ERRORS_EDITOR environment variable is not recognized as a supported Better Errors editor."
+ end
+ if ENV["EDITOR"]
+ editor = editor_from_command(ENV["EDITOR"])
+ return editor if editor
+ puts "EDITOR environment variable is not recognized as a supported Better Errors editor. Using TextMate by default."
+ else
+ puts "Since there is no EDITOR or BETTER_ERRORS_EDITOR environment variable, using Textmate by default."
+ end
+ end
+
+ def self.editor_from_command(editor_command)
+ env_preset = KNOWN_EDITORS.find { |preset| editor_command =~ preset[:sniff] }
+ for_formatting_string(env_preset[:url]) if env_preset
+ end
+
+ def self.editor_from_environment_formatting_string
+ return unless ENV['BETTER_ERRORS_EDITOR_URL']
+
+ for_formatting_string(ENV['BETTER_ERRORS_EDITOR_URL'])
+ end
+
+ def self.editor_from_symbol(symbol)
+ KNOWN_EDITORS.each do |preset|
+ return for_formatting_string(preset[:url]) if preset[:symbols].include?(symbol)
+ end
+ end
+
+ def initialize(url_proc)
+ @url_proc = url_proc
+ end
+
+ def url(raw_path, line)
+ if virtual_path && raw_path.start_with?(virtual_path)
+ if host_path
+ file = raw_path.sub(%r{\A#{virtual_path}}, host_path)
+ else
+ file = raw_path.sub(%r{\A#{virtual_path}/}, '')
+ end
+ else
+ file = raw_path
+ end
+
+ url_proc.call(file, line)
+ end
+
+ def scheme
+ url('/fake', 42).sub(/:.*/, ':')
+ end
+
+ private
+
+ attr_reader :url_proc
+
+ def virtual_path
+ @virtual_path ||= ENV['BETTER_ERRORS_VIRTUAL_PATH']
+ end
+
+ def host_path
+ @host_path ||= ENV['BETTER_ERRORS_HOST_PATH']
+ end
+ end
+end
diff --git a/lib/better_errors/error_page.rb b/lib/better_errors/error_page.rb
index 05a7e7bf..afb28f0d 100644
--- a/lib/better_errors/error_page.rb
+++ b/lib/better_errors/error_page.rb
@@ -1,10 +1,14 @@
require "cgi"
require "json"
require "securerandom"
+require "rouge"
+require "better_errors/error_page_style"
module BetterErrors
# @private
class ErrorPage
+ VariableInfo = Struct.new(:frame, :editor_url, :rails_params, :rack_session, :start_time)
+
def self.template_path(template_name)
File.expand_path("../templates/#{template_name}.erb", __FILE__)
end
@@ -13,6 +17,15 @@ def self.template(template_name)
Erubi::Engine.new(File.read(template_path(template_name)), escape: true)
end
+ def self.render_template(template_name, locals)
+ locals.send(:eval, self.template(template_name).src)
+ rescue => e
+ # Fix the backtrace, which doesn't identify the template that failed (within Better Errors).
+ # We don't know the line number, so just injecting the template path has to be enough.
+ e.backtrace.unshift "#{self.template_path(template_name)}:0"
+ raise
+ end
+
attr_reader :exception, :env, :repls
def initialize(exception, env)
@@ -26,20 +39,21 @@ def id
@id ||= SecureRandom.hex(8)
end
- def render(template_name = "main", csrf_token = nil)
- binding.eval(self.class.template(template_name).src)
- rescue => e
- # Fix the backtrace, which doesn't identify the template that failed (within Better Errors).
- # We don't know the line number, so just injecting the template path has to be enough.
- e.backtrace.unshift "#{self.class.template_path(template_name)}:0"
- raise
+ def render_main(csrf_token, csp_nonce)
+ frame = backtrace_frames[0]
+ first_frame_variable_info = VariableInfo.new(frame, editor_url(frame), rails_params, rack_session, Time.now.to_f)
+ self.class.render_template('main', binding)
+ end
+
+ def render_text
+ self.class.render_template('text', binding)
end
def do_variables(opts)
index = opts["index"].to_i
- @frame = backtrace_frames[index]
- @var_start_time = Time.now.to_f
- { html: render("variable_info") }
+ frame = backtrace_frames[index]
+ variable_info = VariableInfo.new(frame, editor_url(frame), rails_params, rack_session, Time.now.to_f)
+ { html: self.class.render_template("variable_info", variable_info) }
end
def do_eval(opts)
@@ -67,6 +81,10 @@ def exception_message
exception.message.strip.gsub(/(\r?\n\s*\r?\n)+/, "\n")
end
+ def exception_hint
+ exception.hint
+ end
+
def active_support_actions
return [] unless defined?(ActiveSupport::ActionableError)
@@ -90,7 +108,7 @@ def first_frame
private
def editor_url(frame)
- BetterErrors.editor[frame.filename, frame.line]
+ BetterErrors.editor.url(frame.filename, frame.line)
end
def rack_session
@@ -109,11 +127,11 @@ def request_path
env["PATH_INFO"]
end
- def html_formatted_code_block(frame)
+ def self.html_formatted_code_block(frame)
CodeFormatter::HTML.new(frame.filename, frame.line).output
end
- def text_formatted_code_block(frame)
+ def self.text_formatted_code_block(frame)
CodeFormatter::Text.new(frame.filename, frame.line).output
end
@@ -121,7 +139,7 @@ def text_heading(char, str)
str + "\n" + char*str.size
end
- def inspect_value(obj)
+ def self.inspect_value(obj)
if BetterErrors.ignored_classes.include? obj.class.name
"(Instance of ignored class. "\
"#{obj.class.name ? "Remove #{CGI.escapeHTML(obj.class.name)} from" : "Modify"}"\
@@ -141,7 +159,7 @@ def eval_and_respond(index, code)
result, prompt, prefilled_input = @repls[index].send_input(code)
{
- highlighted_input: CodeRay.scan(code, :ruby).div(wrap: nil),
+ highlighted_input: Rouge::Formatters::HTML.new.format(Rouge::Lexers::Ruby.lex(code)),
prefilled_input: prefilled_input,
prompt: prompt,
result: result
diff --git a/lib/better_errors/error_page_style.rb b/lib/better_errors/error_page_style.rb
new file mode 100644
index 00000000..b5c15d88
--- /dev/null
+++ b/lib/better_errors/error_page_style.rb
@@ -0,0 +1,43 @@
+module BetterErrors
+ # @private
+ module ErrorPageStyle
+ def self.compiled_css(for_deployment = false)
+ begin
+ require "sassc"
+ rescue LoadError
+ raise LoadError, "The `sassc` gem is required when developing the `better_errors` gem. "\
+ "If you're using a release of `better_errors`, the compiled CSS is missing from the released gem"
+ # If you arrived here because sassc is not in your project's Gemfile,
+ # the issue here is that the release of the better_errors gem
+ # is supposed to contain the compiled CSS, but that file is missing from the release.
+ # So better_errors is trying to build the CSS on the fly, which requires the sassc gem.
+ #
+ # If you're developing the better_errors gem locally, and you're running a project
+ # that does not have sassc in its bundle, run `rake style:build` in the better_errors
+ # project to compile the CSS file.
+ end
+
+ style_dir = File.expand_path("style", File.dirname(__FILE__))
+ style_file = "#{style_dir}/main.scss"
+
+ engine = SassC::Engine.new(
+ File.read(style_file),
+ filename: style_file,
+ style: for_deployment ? :compressed : :expanded,
+ line_comments: !for_deployment,
+ load_paths: [style_dir],
+ )
+ engine.render
+ end
+
+ def self.style_tag(csp_nonce)
+ style_file = File.expand_path("templates/main.css", File.dirname(__FILE__))
+ css = if File.exist?(style_file)
+ File.open(style_file).read
+ else
+ compiled_css(false)
+ end
+ ""
+ end
+ end
+end
diff --git a/lib/better_errors/exception_hint.rb b/lib/better_errors/exception_hint.rb
new file mode 100644
index 00000000..61ca9935
--- /dev/null
+++ b/lib/better_errors/exception_hint.rb
@@ -0,0 +1,29 @@
+module BetterErrors
+ class ExceptionHint
+ def initialize(exception)
+ @exception = exception
+ end
+
+ def hint
+ case exception
+ when NoMethodError
+ /\Aundefined method `(?[^']+)' for (?[^:]+):(?\w+)/.match(exception.message) do |match|
+ if match[:val] == "nil"
+ return "Something is `nil` when it probably shouldn't be."
+ elsif !match[:klass].start_with? '0x'
+ return "`#{match[:method]}` is being called on a `#{match[:klass]}` object, "\
+ "which might not be the type of object you were expecting."
+ end
+ end
+ when NameError
+ /\Aundefined local variable or method `(?[^']+)' for/.match(exception.message) do |match|
+ return "`#{match[:method]}` is probably misspelled."
+ end
+ end
+ end
+
+ private
+
+ attr_reader :exception
+ end
+end
diff --git a/lib/better_errors/middleware.rb b/lib/better_errors/middleware.rb
index 8215c70b..34b5efa7 100644
--- a/lib/better_errors/middleware.rb
+++ b/lib/better_errors/middleware.rb
@@ -40,7 +40,7 @@ def self.allow_ip!(addr)
allow_ip! "127.0.0.0/8"
allow_ip! "::1/128" rescue nil # windows ruby doesn't have ipv6 support
- CSRF_TOKEN_COOKIE_NAME = "BetterErrors-#{VERSION}-CSRF-Token"
+ CSRF_TOKEN_COOKIE_NAME = "BetterErrors-#{BetterErrors::VERSION}-CSRF-Token"
# A new instance of BetterErrors::Middleware
#
@@ -94,12 +94,13 @@ def protected_app_call(env)
def show_error_page(env, exception=nil)
request = Rack::Request.new(env)
csrf_token = request.cookies[CSRF_TOKEN_COOKIE_NAME] || SecureRandom.uuid
+ csp_nonce = SecureRandom.base64(12)
type, content = if @error_page
if text?(env)
- [ 'plain', @error_page.render('text') ]
+ [ 'plain', @error_page.render_text ]
else
- [ 'html', @error_page.render('main', csrf_token) ]
+ [ 'html', @error_page.render_main(csrf_token, csp_nonce) ]
end
else
[ 'html', no_errors_page ]
@@ -110,7 +111,22 @@ def show_error_page(env, exception=nil)
status_code = ActionDispatch::ExceptionWrapper.new(env, exception).status_code
end
- response = Rack::Response.new(content, status_code, { "Content-Type" => "text/#{type}; charset=utf-8" })
+ headers = {
+ "Content-Type" => "text/#{type}; charset=utf-8",
+ "Content-Security-Policy" => [
+ "default-src 'none'",
+ # Specifying nonce makes a modern browser ignore 'unsafe-inline' which could still be set
+ # for older browsers without nonce support.
+ # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src
+ "script-src 'self' 'nonce-#{csp_nonce}' 'unsafe-inline'",
+ "style-src 'self' 'nonce-#{csp_nonce}' 'unsafe-inline'",
+ "img-src data:",
+ "connect-src 'self'",
+ "navigate-to 'self' #{BetterErrors.editor.scheme}",
+ ].join('; '),
+ }
+
+ response = Rack::Response.new(content, status_code, headers)
unless request.cookies[CSRF_TOKEN_COOKIE_NAME]
response.set_cookie(CSRF_TOKEN_COOKIE_NAME, value: csrf_token, path: "/", httponly: true, same_site: :strict)
diff --git a/lib/better_errors/raised_exception.rb b/lib/better_errors/raised_exception.rb
index f47812f6..8c4a7ccc 100644
--- a/lib/better_errors/raised_exception.rb
+++ b/lib/better_errors/raised_exception.rb
@@ -1,7 +1,9 @@
+require 'better_errors/exception_hint'
+
# @private
module BetterErrors
class RaisedException
- attr_reader :exception, :message, :backtrace
+ attr_reader :exception, :message, :backtrace, :hint
def initialize(exception)
if exception.class.name == "ActionView::Template::Error" && exception.respond_to?(:cause)
@@ -23,6 +25,7 @@ def initialize(exception)
@message = exception.message
setup_backtrace
+ setup_hint
massage_syntax_error
end
@@ -78,5 +81,9 @@ def massage_syntax_error
end
end
end
+
+ def setup_hint
+ @hint = ExceptionHint.new(exception).hint
+ end
end
end
diff --git a/lib/better_errors/style/main.scss b/lib/better_errors/style/main.scss
new file mode 100644
index 00000000..db0d7d1e
--- /dev/null
+++ b/lib/better_errors/style/main.scss
@@ -0,0 +1,746 @@
+/* Basic reset */
+* {
+ 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;
+}
+
+/* ---------------------------------------------------------------------
+ * Basic layout
+ * --------------------------------------------------------------------- */
+
+/* Small */
+@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;
+ }
+}
+
+/* Wide */
+@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
+ * --------------------------------------------------------------------- */
+
+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;
+}
+
+/* Heading */
+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;
+ }
+}
+
+
+/* Light theme */
+/*
+header.exception {
+ text-shadow: 0 1px 0 rgba(250, 250, 250, 0.6);
+ background: rgba(200,100,50,0.10);
+ color: #977;
+}
+header.exception h2 strong {
+ color: #533;
+}
+header.exception p {
+ color: #744;
+}
+*/
+
+/* ---------------------------------------------------------------------
+ * Navigation
+ * --------------------------------------------------------------------- */
+
+.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;
+}
+
+/* ---------------------------------------------------------------------
+ * Sidebar
+ * --------------------------------------------------------------------- */
+
+ul.frames {
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
+}
+
+/* Each item */
+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;
+}
+
+/* Selected frame */
+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;
+}
+
+/* Iconography */
+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);
+}
+
+/* Responsiveness -- flow to single-line mode */
+@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;
+ }
+}
+
+/* ---------------------------------------------------------------------
+ * Monospace
+ * --------------------------------------------------------------------- */
+
+pre, code, .be-repl input, .be-repl .command-line span, textarea, .code_linenums {
+ font-family: menlo, lucida console, monospace;
+ font-size: 8pt;
+}
+
+/* ---------------------------------------------------------------------
+ * Display area
+ * --------------------------------------------------------------------- */
+
+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;
+}
+
+/* Titlebar */
+.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;
+
+ @import "syntax_highlighting";
+}
+.code {
+ // For now, the syntax-highlighted console only supports light mode.
+ // Once the entire page has a dark theme, this should change.
+ @media (prefers-color-scheme: dark) {
+ @import "syntax_highlighting_dark";
+ }
+}
+
+.code {
+ margin-bottom: -1px;
+ border-top-left-radius:2px;
+ padding: 10px 0;
+ overflow: auto;
+
+ .code-wrapper {
+ // This fixes the highlight or other background of the pre not stretching the full scrollable width.
+ display: inline-block;
+ min-width: 100%;
+ }
+
+ pre {
+ padding-left:12px;
+ min-height:16px;
+ }
+}
+
+/* Source unavailable */
+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;
+}
+
+$code-highlight-background: rgba(51, 136, 170, 0.15);
+$code-highlight-background-flash: adjust-color($code-highlight-background, $alpha: 0.3);
+
+@-webkit-keyframes highlight {
+ 0% { background: $code-highlight-background-flash; }
+ 100% { background: $code-highlight-background; }
+}
+@-moz-keyframes highlight {
+ 0% { background: $code-highlight-background-flash; }
+ 100% { background: $code-highlight-background; }
+}
+@keyframes highlight {
+ 0% { background: $code-highlight-background-flash; }
+ 100% { background: $code-highlight-background; }
+}
+
+.code .highlight, .code_linenums .highlight {
+ background: $code-highlight-background;
+ -webkit-animation: highlight 400ms linear 1;
+ -moz-animation: highlight 400ms linear 1;
+ animation: highlight 400ms linear 1;
+}
+
+/* REPL shell */
+.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;
+}
+
+/* .command-line > span + input */
+.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%;
+}
+
+/* Input box */
+.be-console input,
+.be-console input:focus {
+ outline: 0;
+ border: 0;
+ padding: 0;
+ background: transparent;
+ margin: 0;
+}
+
+/* Hint text */
+.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;
+}
+
+/* ---------------------------------------------------------------------
+ * Variable infos
+ * --------------------------------------------------------------------- */
+
+.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;
+}
+
+/* "(object doesn't support inspect)" */
+.sub .unsupported {
+ font-family: sans-serif;
+ color: #777;
+}
+
+/* ---------------------------------------------------------------------
+ * Scrollbar
+ * --------------------------------------------------------------------- */
+
+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.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;
+}
diff --git a/lib/better_errors/style/syntax_highlighting.scss b/lib/better_errors/style/syntax_highlighting.scss
new file mode 100644
index 00000000..894a91e9
--- /dev/null
+++ b/lib/better_errors/style/syntax_highlighting.scss
@@ -0,0 +1,81 @@
+// Thanks to https://github.com/jwarby/jekyll-pygments-themes/blob/master/pastie.css
+
+$base01: #586e75;
+$base1: #93a1a1;
+$base3: #fdf6e3;
+$orange: #cb4b16;
+$red: #dc322f;
+$blue: #268bd2;
+$cyan: #2aa198;
+$green: #859900;
+$yellow: #B58900;
+
+& { background-color: $base3; color: $base01 }
+.c { color: $base1 } /* Comment */
+.err { color: $base01 } /* Error */
+.g { color: $base01 } /* Generic */
+.k { color: $green } /* Keyword */
+.l { color: $base01 } /* Literal */
+.n { color: $base01 } /* Name */
+.o { color: $green } /* Operator */
+.x { color: $orange } /* Other */
+.p { color: $base01 } /* Punctuation */
+.cm { color: $base1 } /* Comment.Multiline */
+.cp { color: $green } /* Comment.Preproc */
+.c1 { color: $base1 } /* Comment.Single */
+.cs { color: $green } /* Comment.Special */
+.gd { color: $cyan } /* Generic.Deleted */
+.ge { color: $base01; font-style: italic } /* Generic.Emph */
+.gr { color: $red } /* Generic.Error */
+.gh { color: $orange } /* Generic.Heading */
+.gi { color: $green } /* Generic.Inserted */
+.go { color: $base01 } /* Generic.Output */
+.gp { color: $base01 } /* Generic.Prompt */
+.gs { color: $base01; font-weight: bold } /* Generic.Strong */
+.gu { color: $orange } /* Generic.Subheading */
+.gt { color: $base01 } /* Generic.Traceback */
+.kc { color: $orange } /* Keyword.Constant */
+.kd { color: $blue } /* Keyword.Declaration */
+.kn { color: $green } /* Keyword.Namespace */
+.kp { color: $green } /* Keyword.Pseudo */
+.kr { color: $blue } /* Keyword.Reserved */
+.kt { color: $red } /* Keyword.Type */
+.ld { color: $base01 } /* Literal.Date */
+.m { color: $cyan } /* Literal.Number */
+.s { color: $cyan } /* Literal.String */
+.na { color: $base01 } /* Name.Attribute */
+.nb { color: $yellow } /* Name.Builtin */
+.nc { color: $blue } /* Name.Class */
+.no { color: $orange } /* Name.Constant */
+.nd { color: $blue } /* Name.Decorator */
+.ni { color: $orange } /* Name.Entity */
+.ne { color: $orange } /* Name.Exception */
+.nf { color: $blue } /* Name.Function */
+.nl { color: $base01 } /* Name.Label */
+.nn { color: $base01 } /* Name.Namespace */
+.nx { color: $base01 } /* Name.Other */
+.py { color: $base01 } /* Name.Property */
+.nt { color: $blue } /* Name.Tag */
+.nv { color: $blue } /* Name.Variable */
+.ow { color: $green } /* Operator.Word */
+.w { color: $base01 } /* Text.Whitespace */
+.mf { color: $cyan } /* Literal.Number.Float */
+.mh { color: $cyan } /* Literal.Number.Hex */
+.mi { color: $cyan } /* Literal.Number.Integer */
+.mo { color: $cyan } /* Literal.Number.Oct */
+.sb { color: $base1 } /* Literal.String.Backtick */
+.sc { color: $cyan } /* Literal.String.Char */
+.sd { color: $base01 } /* Literal.String.Doc */
+.s2 { color: $cyan } /* Literal.String.Double */
+.se { color: $orange } /* Literal.String.Escape */
+.sh { color: $base01 } /* Literal.String.Heredoc */
+.si { color: $cyan } /* Literal.String.Interpol */
+.sx { color: $cyan } /* Literal.String.Other */
+.sr { color: $red } /* Literal.String.Regex */
+.s1 { color: $cyan } /* Literal.String.Single */
+.ss { color: $cyan } /* Literal.String.Symbol */
+.bp { color: $blue } /* Name.Builtin.Pseudo */
+.vc { color: $blue } /* Name.Variable.Class */
+.vg { color: $blue } /* Name.Variable.Global */
+.vi { color: $blue } /* Name.Variable.Instance */
+.il { color: $cyan } /* Literal.Number.Integer.Long */
diff --git a/lib/better_errors/style/syntax_highlighting_dark.scss b/lib/better_errors/style/syntax_highlighting_dark.scss
new file mode 100644
index 00000000..664d30db
--- /dev/null
+++ b/lib/better_errors/style/syntax_highlighting_dark.scss
@@ -0,0 +1,84 @@
+// Thanks to https://gist.github.com/nicolashery/5765395
+
+// Since the light theme is applied before the dark theme, the dark theme is additive.
+// So we must set the background color and font weight on everything that the light theme might have changed.
+
+$base03: #002b36;
+$base01: #586e75;
+$base1: #93a1a1;
+$orange: #cb4b16;
+$red: #dc322f;
+$blue: #268bd2;
+$cyan: #2aa198;
+$green: #859900;
+$yellow: #B58900;
+
+& { background-color: $base03; color: $base1; }
+.c { color: $base01; background-color: transparent; font-style: inherit; } /* Comment */
+.err { color: $base1; background-color: transparent; font-style: inherit; } /* Error */
+.g { color: $base1; background-color: transparent; font-style: inherit; } /* Generic */
+.k { color: $green; background-color: transparent; font-style: inherit; } /* Keyword */
+.l { color: $base1; background-color: transparent; font-style: inherit; } /* Literal */
+.n { color: $base1; background-color: transparent; font-style: inherit; } /* Name */
+.o { color: $green; background-color: transparent; font-style: inherit; } /* Operator */
+.x { color: $orange; background-color: transparent; font-style: inherit; } /* Other */
+.p { color: $base1; background-color: transparent; font-style: inherit; } /* Punctuation */
+.cm { color: $base01; background-color: transparent; font-style: inherit; } /* Comment.Multiline */
+.cp { color: $green; background-color: transparent; font-style: inherit; } /* Comment.Preproc */
+.c1 { color: $base01; background-color: transparent; font-style: inherit; } /* Comment.Single */
+.cs { color: $green; background-color: transparent; font-style: inherit; } /* Comment.Special */
+.gd { color: $cyan; background-color: transparent; font-style: inherit; } /* Generic.Deleted */
+.ge { color: $base1; background-color: transparent; font-style: italic; } /* Generic.Emph */
+.gr { color: $red; background-color: transparent; font-style: inherit; } /* Generic.Error */
+.gh { color: $orange; background-color: transparent; font-style: inherit; } /* Generic.Heading */
+.gi { color: $green; background-color: transparent; font-style: inherit; } /* Generic.Inserted */
+.go { color: $base1; background-color: transparent; font-style: inherit; } /* Generic.Output */
+.gp { color: $base1; background-color: transparent; font-style: inherit; } /* Generic.Prompt */
+.gs { color: $base1; background-color: transparent; font-weight: bold; } /* Generic.Strong */
+.gu { color: $orange; background-color: transparent; font-style: inherit; } /* Generic.Subheading */
+.gt { color: $base1; background-color: transparent; font-style: inherit; } /* Generic.Traceback */
+.kc { color: $orange; background-color: transparent; font-style: inherit; } /* Keyword.Constant */
+.kd { color: $blue; background-color: transparent; font-style: inherit; } /* Keyword.Declaration */
+.kn { color: $green; background-color: transparent; font-style: inherit; } /* Keyword.Namespace */
+.kp { color: $green; background-color: transparent; font-style: inherit; } /* Keyword.Pseudo */
+.kr { color: $blue; background-color: transparent; font-style: inherit; } /* Keyword.Reserved */
+.kt { color: $red; background-color: transparent; font-style: inherit; } /* Keyword.Type */
+.ld { color: $base1; background-color: transparent; font-style: inherit; } /* Literal.Date */
+.m { color: $cyan; background-color: transparent; font-style: inherit; } /* Literal.Number */
+.s { color: $cyan; background-color: transparent; font-style: inherit; } /* Literal.String */
+.na { color: $base1; background-color: transparent; font-style: inherit; } /* Name.Attribute */
+.nb { color: $yellow; background-color: transparent; font-style: inherit; } /* Name.Builtin */
+.nc { color: $blue; background-color: transparent; font-style: inherit; } /* Name.Class */
+.no { color: $orange; background-color: transparent; font-style: inherit; } /* Name.Constant */
+.nd { color: $blue; background-color: transparent; font-style: inherit; } /* Name.Decorator */
+.ni { color: $orange; background-color: transparent; font-style: inherit; } /* Name.Entity */
+.ne { color: $orange; background-color: transparent; font-style: inherit; } /* Name.Exception */
+.nf { color: $blue; background-color: transparent; font-style: inherit; } /* Name.Function */
+.nl { color: $base1; background-color: transparent; font-style: inherit; } /* Name.Label */
+.nn { color: $base1; background-color: transparent; font-style: inherit; } /* Name.Namespace */
+.nx { color: $base1; background-color: transparent; font-style: inherit; } /* Name.Other */
+.py { color: $base1; background-color: transparent; font-style: inherit; } /* Name.Property */
+.nt { color: $blue; background-color: transparent; font-style: inherit; } /* Name.Tag */
+.nv { color: $blue; background-color: transparent; font-style: inherit; } /* Name.Variable */
+.ow { color: $green; background-color: transparent; font-style: inherit; } /* Operator.Word */
+.w { color: $base1; background-color: transparent; font-style: inherit; } /* Text.Whitespace */
+.mf { color: $cyan; background-color: transparent; font-style: inherit; } /* Literal.Number.Float */
+.mh { color: $cyan; background-color: transparent; font-style: inherit; } /* Literal.Number.Hex */
+.mi { color: $cyan; background-color: transparent; font-style: inherit; } /* Literal.Number.Integer */
+.mo { color: $cyan; background-color: transparent; font-style: inherit; } /* Literal.Number.Oct */
+.sb { color: $base01; background-color: transparent; font-style: inherit; } /* Literal.String.Backtick */
+.sc { color: $cyan; background-color: transparent; font-style: inherit; } /* Literal.String.Char */
+.sd { color: $base1; background-color: transparent; font-style: inherit; } /* Literal.String.Doc */
+.s2 { color: $cyan; background-color: transparent; font-style: inherit; } /* Literal.String.Double */
+.se { color: $orange; background-color: transparent; font-style: inherit; } /* Literal.String.Escape */
+.sh { color: $base1; background-color: transparent; font-style: inherit; } /* Literal.String.Heredoc */
+.si { color: $cyan; background-color: transparent; font-style: inherit; } /* Literal.String.Interpol */
+.sx { color: $cyan; background-color: transparent; font-style: inherit; } /* Literal.String.Other */
+.sr { color: $red; background-color: transparent; font-style: inherit; } /* Literal.String.Regex */
+.s1 { color: $cyan; background-color: transparent; font-style: inherit; } /* Literal.String.Single */
+.ss { color: $cyan; background-color: transparent; font-style: inherit; } /* Literal.String.Symbol */
+.bp { color: $blue; background-color: transparent; font-style: inherit; } /* Name.Builtin.Pseudo */
+.vc { color: $blue; background-color: transparent; font-style: inherit; } /* Name.Variable.Class */
+.vg { color: $blue; background-color: transparent; font-style: inherit; } /* Name.Variable.Global */
+.vi { color: $blue; background-color: transparent; font-style: inherit; } /* Name.Variable.Instance */
+.il { color: $cyan; background-color: transparent; font-style: inherit; } /* Literal.Number.Integer.Long */
diff --git a/lib/better_errors/templates/main.erb b/lib/better_errors/templates/main.erb
index 1ab68559..37dc30de 100644
--- a/lib/better_errors/templates/main.erb
+++ b/lib/better_errors/templates/main.erb
@@ -2,714 +2,14 @@
<%= exception_type %> at <%= request_path %>
+
-
+
<%# Stylesheets are placed in the for Turbolinks compatibility. %>
-
+ <%== ErrorPageStyle.style_tag(csp_nonce) %>
<%# IE8 compatibility crap %>
-
+
+
+ Better Errors can't apply inline style (or run Javascript) ,
+ possibly because you have a Content Security Policy along with Turbolinks.
+ But you can
+ open the interactive console in a new tab/window .
+
+
+
<%= exception_type %> at <%= request_path %>
@@ -764,6 +73,9 @@
<% end %>
<% end %>
+ <% if exception_hint %>
+ Hint: <%= exception_hint %>
+ <% end %>
@@ -791,21 +103,37 @@
- <% backtrace_frames.each_with_index do |frame, index| %>
-
- <% end %>
+
+
+
+ Better Errors can't run Javascript here (or apply inline style) ,
+ possibly because you have a Content Security Policy along with Turbolinks.
+ But you can
+ open the interactive console in a new tab/window .
+
+
+ <%== ErrorPage.render_template('variable_info', first_frame_variable_info) %>
+
+
-