lib/sass/error.rb
module Sass # An exception class that keeps track of # the line of the Sass template it was raised on # and the Sass file that was being parsed (if applicable). # # All Sass errors are raised as {Sass::SyntaxError}s. # # When dealing with SyntaxErrors, # it's important to provide filename and line number information. # This will be used in various error reports to users, including backtraces; # see \{#sass\_backtrace} for details. # # Some of this information is usually provided as part of the constructor. # New backtrace entries can be added with \{#add\_backtrace}, # which is called when an exception is raised between files (e.g. with `@import`). # # Often, a chunk of code will all have similar backtrace information - # the same filename or even line. # It may also be useful to have a default line number set. # In those situations, the default values can be used # by omitting the information on the original exception, # and then calling \{#modify\_backtrace} in a wrapper `rescue`. # When doing this, be sure that all exceptions ultimately end up # with the information filled in. class SyntaxError < StandardError # The backtrace of the error within Sass files. # This is an array of hashes containing information for a single entry. # The hashes have the following keys: # # `:filename` # : The name of the file in which the exception was raised, # or `nil` if no filename is available. # # `:mixin` # : The name of the mixin in which the exception was raised, # or `nil` if it wasn't raised in a mixin. # # `:line` # : The line of the file on which the error occurred. Never nil. # # This information is also included in standard backtrace format # in the output of \{#backtrace}. # # @return [Aray<{Symbol => Object>}] attr_accessor :sass_backtrace # The text of the template where this error was raised. # # @return [String] attr_accessor :sass_template # @param msg [String] The error message # @param attrs [{Symbol => Object}] The information in the backtrace entry. # See \{#sass\_backtrace} def initialize(msg, attrs = {}) @message = msg @sass_backtrace = [] add_backtrace(attrs) end # The name of the file in which the exception was raised. # This could be `nil` if no filename is available. # # @return [String, nil] def sass_filename sass_backtrace.first[:filename] end # The name of the mixin in which the error occurred. # This could be `nil` if the error occurred outside a mixin. # # @return [Fixnum] def sass_mixin sass_backtrace.first[:mixin] end # The line of the Sass template on which the error occurred. # # @return [Fixnum] def sass_line sass_backtrace.first[:line] end # Adds an entry to the exception's Sass backtrace. # # @param attrs [{Symbol => Object}] The information in the backtrace entry. # See \{#sass\_backtrace} def add_backtrace(attrs) sass_backtrace << attrs.reject {|k, v| v.nil?} end # Modify the top Sass backtrace entries # (that is, the most deeply nested ones) # to have the given attributes. # # Specifically, this goes through the backtrace entries # from most deeply nested to least, # setting the given attributes for each entry. # If an entry already has one of the given attributes set, # the pre-existing attribute takes precedence # and is not used for less deeply-nested entries # (even if they don't have that attribute set). # # @param attrs [{Symbol => Object}] The information to add to the backtrace entry. # See \{#sass\_backtrace} def modify_backtrace(attrs) attrs = attrs.reject {|k, v| v.nil?} # Move backwards through the backtrace (0...sass_backtrace.size).to_a.reverse.each do |i| entry = sass_backtrace[i] sass_backtrace[i] = attrs.merge(entry) attrs.reject! {|k, v| entry.include?(k)} break if attrs.empty? end end # @return [String] The error message def to_s @message end # Returns the standard exception backtrace, # including the Sass backtrace. # # @return [Array<String>] def backtrace return nil if super.nil? sass_backtrace.map do |h| "#{h[:filename] || "(sass)"}:#{h[:line]}" + (h[:mixin] ? ":in `#{h[:mixin]}'" : "") end + super end # Returns a string representation of the Sass backtrace. # # @param default_filename [String] The filename to use for unknown files # @see #sass_backtrace # @return [String] def sass_backtrace_str(default_filename = "an unknown file") lines = self.message.split("\n") msg = lines[0] + lines[1..-1]. map {|l| "\n" + (" " * "Syntax error: ".size) + l}.join "Syntax error: #{msg}" + Haml::Util.enum_with_index(sass_backtrace).map do |entry, i| "\n #{i == 0 ? "on" : "from"} line #{entry[:line]}" + " of #{entry[:filename] || default_filename}" + (entry[:mixin] ? ", in `#{entry[:mixin]}'" : "") end.join end class << self # Returns an error report for an exception in CSS format. # # @param e [Exception] # @param options [{Symbol => Object}] The options passed to {Sass::Engine#initialize} # @return [String] The error report # @raise [Exception] `e`, if the # {file:SASS_REFERENCE.md#full_exception-option `:full_exception`} option # is set to false. def exception_to_css(e, options) raise e unless options[:full_exception] header = header_string(e, options) <<END /* #{header} Backtrace:\n#{e.backtrace.join("\n")} */ body:before { white-space: pre; font-family: monospace; content: "#{header.gsub('"', '\"').gsub("\n", '\\A ')}"; } END end private def header_string(e, options) return "#{e.class}: #{e.message}" unless e.is_a? Sass::SyntaxError line_offset = options[:line] || 1 line_num = e.sass_line + 1 - line_offset min = [line_num - 6, 0].max section = e.sass_template.rstrip.split("\n")[min ... line_num + 5] return e.sass_backtrace_str if section.nil? || section.empty? e.sass_backtrace_str + "\n\n" + Haml::Util.enum_with_index(section). map {|line, i| "#{line_offset + min + i}: #{line}"}.join("\n") end end end # The class for Sass errors that are raised due to invalid unit conversions # in SassScript. class UnitConversionError < SyntaxError; end end