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}
+
#{super}
+ } + 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) %> +
+
-