blob: 7ee7460356b663f4cba50c76edaaab00c168ce5e [file] [log] [blame]
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 [String]
def sass_mixin
sass_backtrace.first[:mixin]
end
# The line of the Sass template on which the error occurred.
#
# @return [Integer]
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?
return super if sass_backtrace.all? {|h| h.empty?}
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 = message.split("\n")
msg = lines[0] + lines[1..-1].
map {|l| "\n" + (" " * "Error: ".size) + l}.join
"Error: #{msg}" +
sass_backtrace.each_with_index.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 line_offset [Integer] The number of the first line of the Sass template.
# @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, line_offset = 1)
header = header_string(e, line_offset)
<<END
/*
#{header.gsub('*/', '*\\/')}
Backtrace:\n#{e.backtrace.join("\n").gsub('*/', '*\\/')}
*/
body:before {
white-space: pre;
font-family: monospace;
content: "#{header.gsub('"', '\"').gsub("\n", '\\A ')}"; }
END
end
private
def header_string(e, line_offset)
unless e.is_a?(Sass::SyntaxError) && e.sass_line && e.sass_template
return "#{e.class}: #{e.message}"
end
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" + section.each_with_index.
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