vendor/plugins/haml/lib/sass/engine.rb in radiant-0.8.2 vs vendor/plugins/haml/lib/sass/engine.rb in radiant-0.9.0.rc2
- old
+ new
@@ -1,466 +1,515 @@
+require 'strscan'
+require 'digest/sha1'
require 'sass/tree/node'
-require 'sass/tree/value_node'
require 'sass/tree/rule_node'
require 'sass/tree/comment_node'
-require 'sass/tree/attr_node'
+require 'sass/tree/prop_node'
require 'sass/tree/directive_node'
-require 'sass/constant'
+require 'sass/tree/variable_node'
+require 'sass/tree/mixin_def_node'
+require 'sass/tree/mixin_node'
+require 'sass/tree/if_node'
+require 'sass/tree/while_node'
+require 'sass/tree/for_node'
+require 'sass/tree/debug_node'
+require 'sass/tree/import_node'
+require 'sass/environment'
+require 'sass/script'
require 'sass/error'
+require 'sass/files'
+require 'haml/shared'
module Sass
- # This is the class where all the parsing and processing of the Sass
- # template is done. It can be directly used by the user by creating a
- # new instance and calling <tt>render</tt> to render the template. For example:
+ # A Sass mixin.
#
- # template = File.load('stylesheets/sassy.sass')
- # sass_engine = Sass::Engine.new(template)
- # output = sass_engine.render
- # puts output
+ # `name`: `String`
+ # : The name of the mixin.
+ #
+ # `args`: `Array<(String, Script::Node)>`
+ # : The arguments for the mixin.
+ # Each element is a tuple containing the name of the argument
+ # and the parse tree for the default value of the argument.
+ #
+ # `environment`: {Sass::Environment}
+ # : The environment in which the mixin was defined.
+ # This is captured so that the mixin can have access
+ # to local variables defined in its scope.
+ #
+ # `tree`: {Sass::Tree::Node}
+ # : The parse tree for the mixin.
+ Mixin = Struct.new(:name, :args, :environment, :tree)
+
+ # This class handles the parsing and compilation of the Sass template.
+ # Example usage:
+ #
+ # template = File.load('stylesheets/sassy.sass')
+ # sass_engine = Sass::Engine.new(template)
+ # output = sass_engine.render
+ # puts output
class Engine
- # The character that begins a CSS attribute.
- ATTRIBUTE_CHAR = ?:
+ include Haml::Util
+ # A line of Sass code.
+ #
+ # `text`: `String`
+ # : The text in the line, without any whitespace at the beginning or end.
+ #
+ # `tabs`: `Fixnum`
+ # : The level of indentation of the line.
+ #
+ # `index`: `Fixnum`
+ # : The line number in the original document.
+ #
+ # `offset`: `Fixnum`
+ # : The number of bytes in on the line that the text begins.
+ # This ends up being the number of bytes of leading whitespace.
+ #
+ # `filename`: `String`
+ # : The name of the file in which this line appeared.
+ #
+ # `children`: `Array<Line>`
+ # : The lines nested below this one.
+ class Line < Struct.new(:text, :tabs, :index, :offset, :filename, :children)
+ def comment?
+ text[0] == COMMENT_CHAR && (text[1] == SASS_COMMENT_CHAR || text[1] == CSS_COMMENT_CHAR)
+ end
+ end
+
+ # The character that begins a CSS property.
+ # @private
+ PROPERTY_CHAR = ?:
+
# The character that designates that
- # an attribute should be assigned to the result of constant arithmetic.
+ # a property should be assigned to a SassScript expression.
+ # @private
SCRIPT_CHAR = ?=
# The character that designates the beginning of a comment,
# either Sass or CSS.
+ # @private
COMMENT_CHAR = ?/
# The character that follows the general COMMENT_CHAR and designates a Sass comment,
# which is not output as a CSS comment.
+ # @private
SASS_COMMENT_CHAR = ?/
# The character that follows the general COMMENT_CHAR and designates a CSS comment,
# which is embedded in the CSS document.
+ # @private
CSS_COMMENT_CHAR = ?*
# The character used to denote a compiler directive.
+ # @private
DIRECTIVE_CHAR = ?@
# Designates a non-parsed rule.
+ # @private
ESCAPE_CHAR = ?\\
# Designates block as mixin definition rather than CSS rules to output
+ # @private
MIXIN_DEFINITION_CHAR = ?=
# Includes named mixin declared using MIXIN_DEFINITION_CHAR
+ # @private
MIXIN_INCLUDE_CHAR = ?+
+ # The regex that matches properties of the form `name: prop`.
+ # @private
+ PROPERTY_NEW_MATCHER = /^[^\s:"\[]+\s*[=:](\s|$)/
+
# The regex that matches and extracts data from
- # attributes of the form <tt>:name attr</tt>.
- ATTRIBUTE = /^:([^\s=:]+)\s*(=?)(?:\s+|$)(.*)/
+ # properties of the form `name: prop`.
+ # @private
+ PROPERTY_NEW = /^([^\s=:"]+)(\s*=|:)(?:\s+|$)(.*)/
- # The regex that matches attributes of the form <tt>name: attr</tt>.
- ATTRIBUTE_ALTERNATE_MATCHER = /^[^\s:]+\s*[=:](\s|$)/
-
# The regex that matches and extracts data from
- # attributes of the form <tt>name: attr</tt>.
- ATTRIBUTE_ALTERNATE = /^([^\s=:]+)(\s*=|:)(?:\s+|$)(.*)/
+ # properties of the form `:name prop`.
+ # @private
+ PROPERTY_OLD = /^:([^\s=:"]+)\s*(=?)(?:\s+|$)(.*)/
- # Creates a new instace of Sass::Engine that will compile the given
- # template string when <tt>render</tt> is called.
- # See README.rdoc for available options.
- #
- #--
- #
- # TODO: Add current options to REFRENCE. Remember :filename!
- #
- # When adding options, remember to add information about them
- # to README.rdoc!
- #++
- #
+ # The default options for Sass::Engine.
+ DEFAULT_OPTIONS = {
+ :style => :nested,
+ :load_paths => ['.'],
+ :cache => true,
+ :cache_location => './.sass-cache',
+ }.freeze
+
+ # @param template [String] The Sass template.
+ # @param options [{Symbol => Object}] An options hash;
+ # see {file:SASS_REFERENCE.md#sass_options the Sass options documentation}
def initialize(template, options={})
- @options = {
- :style => :nested,
- :load_paths => ['.']
- }.merge! options
- @template = template.split(/\r\n|\r|\n/)
- @lines = []
- @constants = {"important" => "!important"}
- @mixins = {}
+ @options = DEFAULT_OPTIONS.merge(options.reject {|k, v| v.nil?})
+ @template = template
+
+ # Support both, because the docs said one and the other actually worked
+ # for quite a long time.
+ @options[:line_comments] ||= @options[:line_numbers]
+
+ # Backwards compatibility
+ @options[:property_syntax] ||= @options[:attribute_syntax]
+ case @options[:property_syntax]
+ when :alternate; @options[:property_syntax] = :new
+ when :normal; @options[:property_syntax] = :old
+ end
end
- # Processes the template and returns the result as a string.
+ # Render the template to CSS.
+ #
+ # @return [String] The CSS
+ # @raise [Sass::SyntaxError] if there's an error in the document
def render
- begin
- render_to_tree.to_s
- rescue SyntaxError => err
- unless err.sass_filename
- err.add_backtrace_entry(@options[:filename])
- end
- raise err
- end
+ to_tree.render
end
alias_method :to_css, :render
- protected
-
- def constants
- @constants
+ # Parses the document into its parse tree.
+ #
+ # @return [Sass::Tree::Node] The root of the parse tree.
+ # @raise [Sass::SyntaxError] if there's an error in the document
+ def to_tree
+ root = Tree::Node.new
+ append_children(root, tree(tabulate(@template)).first, true)
+ root.options = @options
+ root
+ rescue SyntaxError => e; e.add_metadata(@options[:filename], @line)
end
- def mixins
- @mixins
- end
+ private
- def render_to_tree
- split_lines
+ def tabulate(string)
+ tab_str = nil
+ comment_tab_str = nil
+ first = true
+ lines = []
+ string.gsub(/\r|\n|\r\n|\r\n/, "\n").scan(/^.*?$/).each_with_index do |line, index|
+ index += (@options[:line] || 1)
+ if line.strip.empty?
+ lines.last.text << "\n" if lines.last && lines.last.comment?
+ next
+ end
- root = Tree::Node.new(@options[:style])
- index = 0
- while @lines[index]
- old_index = index
- child, index = build_tree(index)
+ line_tab_str = line[/^\s*/]
+ unless line_tab_str.empty?
+ if tab_str.nil?
+ comment_tab_str ||= line_tab_str
+ next if try_comment(line, lines.last, "", comment_tab_str, index)
+ comment_tab_str = nil
+ end
- if child.is_a? Tree::Node
- child.line = old_index + 1
- root << child
- elsif child.is_a? Array
- child.each do |c|
- root << c
+ tab_str ||= line_tab_str
+
+ raise SyntaxError.new("Indenting at the beginning of the document is illegal.", index) if first
+ if tab_str.include?(?\s) && tab_str.include?(?\t)
+ raise SyntaxError.new("Indentation can't use both tabs and spaces.", index)
end
end
- end
- @lines.clear
+ first &&= !tab_str.nil?
+ if tab_str.nil?
+ lines << Line.new(line.strip, 0, index, 0, @options[:filename], [])
+ next
+ end
- root
+ comment_tab_str ||= line_tab_str
+ if try_comment(line, lines.last, tab_str * (lines.last.tabs + 1), comment_tab_str, index)
+ next
+ else
+ comment_tab_str = nil
+ end
+
+ line_tabs = line_tab_str.scan(tab_str).size
+ raise SyntaxError.new(<<END.strip.gsub("\n", ' '), index) if tab_str * line_tabs != line_tab_str
+Inconsistent indentation: #{Haml::Shared.human_indentation line_tab_str, true} used for indentation,
+but the rest of the document was indented using #{Haml::Shared.human_indentation tab_str}.
+END
+ lines << Line.new(line.strip, line_tabs, index, tab_str.size, @options[:filename], [])
+ end
+ lines
end
- private
+ def try_comment(line, last, tab_str, comment_tab_str, index)
+ return unless last && last.comment?
+ return unless line =~ /^#{tab_str}/
+ unless line =~ /^(?:#{comment_tab_str})(.*)$/
+ raise SyntaxError.new(<<MSG.strip.gsub("\n", " "), index)
+Inconsistent indentation:
+previous line was indented by #{Haml::Shared.human_indentation comment_tab_str},
+but this line was indented by #{Haml::Shared.human_indentation line[/^\s*/]}.
+MSG
+ end
- # Readies each line in the template for parsing,
- # and computes the tabulation of the line.
- def split_lines
- @line = 0
- old_tabs = nil
- @template.each_with_index do |line, index|
- @line += 1
+ last.text << "\n" << $1
+ true
+ end
- tabs = count_tabs(line)
+ def tree(arr, i = 0)
+ return [], i if arr[i].nil?
- if line[0] == COMMENT_CHAR && line[1] == SASS_COMMENT_CHAR && tabs == 0
- tabs = old_tabs
- end
-
- if tabs # if line isn't blank
- raise SyntaxError.new("Indenting at the beginning of the document is illegal.", @line) if old_tabs.nil? && tabs > 0
-
- if old_tabs && tabs - old_tabs > 1
- raise SyntaxError.new("#{tabs * 2} spaces were used for indentation. Sass must be indented using two spaces.", @line)
+ base = arr[i].tabs
+ nodes = []
+ while (line = arr[i]) && line.tabs >= base
+ if line.tabs > base
+ if line.tabs > base + 1
+ raise SyntaxError.new("The line was indented #{line.tabs - base} levels deeper than the previous line.", line.index)
end
- @lines << [line.strip, tabs]
- old_tabs = tabs
+ nodes.last.children, i = tree(arr, i)
else
- @lines << ['//', old_tabs || 0]
+ nodes << line
+ i += 1
end
end
-
- @line = nil
+ return nodes, i
end
- # Counts the tabulation of a line.
- def count_tabs(line)
- return nil if line.strip.empty?
- return nil unless spaces = line.index(/[^ ]/)
+ def build_tree(parent, line, root = false)
+ @line = line.index
+ node_or_nodes = parse_line(parent, line, root)
- if spaces % 2 == 1
- raise SyntaxError.new(<<END.strip, @line)
-#{spaces} space#{spaces == 1 ? ' was' : 's were'} used for indentation. Sass must be indented using two spaces.
-END
- elsif line[spaces] == ?\t
- raise SyntaxError.new(<<END.strip, @line)
-A tab character was used for indentation. Sass must be indented using two spaces.
-Are you sure you have soft tabs enabled in your editor?
-END
- end
- spaces / 2
- end
+ Array(node_or_nodes).each do |node|
+ # Node is a symbol if it's non-outputting, like a variable assignment
+ next unless node.is_a? Tree::Node
- def build_tree(index)
- line, tabs = @lines[index]
- index += 1
- @line = index
- node = parse_line(line)
+ node.line = line.index
+ node.filename = line.filename
- has_children = has_children?(index, tabs)
-
- # Node is a symbol if it's non-outputting, like a constant assignment
- unless node.is_a? Tree::Node
- if has_children
- if node == :constant
- raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath constants.", @line + 1)
- elsif node.is_a? Array
- # arrays can either be full of import statements
- # or attributes from mixin includes
- # in either case they shouldn't have children.
- # Need to peek into the array in order to give meaningful errors
- directive_type = (node.first.is_a?(Tree::DirectiveNode) ? "import" : "mixin")
- raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath #{directive_type} directives.", @line + 1)
- end
+ if node.is_a?(Tree::CommentNode)
+ node.lines = line.children
+ else
+ append_children(node, line.children, false)
end
-
- index = @line if node == :mixin
- return node, index
end
- node.line = @line
+ node_or_nodes
+ end
- if node.is_a? Tree::CommentNode
- while has_children
- line, index = raw_next_line(index)
- node << line
+ def append_children(parent, children, root)
+ continued_rule = nil
+ children.each do |line|
+ child = build_tree(parent, line, root)
- has_children = has_children?(index, tabs)
+ if child.is_a?(Tree::RuleNode) && child.continued?
+ raise SyntaxError.new("Rules can't end in commas.", child.line) unless child.children.empty?
+ if continued_rule
+ continued_rule.add_rules child
+ else
+ continued_rule = child
+ end
+ next
end
- return node, index
- end
-
- # Resolve multiline rules
- if node.is_a?(Tree::RuleNode)
- if node.continued?
- child, index = build_tree(index) if @lines[old_index = index]
- if @lines[old_index].nil? || has_children?(old_index, tabs) || !child.is_a?(Tree::RuleNode)
- raise SyntaxError.new("Rules can't end in commas.", @line)
- end
-
- node.add_rules child
+ if continued_rule
+ raise SyntaxError.new("Rules can't end in commas.", continued_rule.line) unless child.is_a?(Tree::RuleNode)
+ continued_rule.add_rules child
+ continued_rule.children = child.children
+ continued_rule, child = nil, continued_rule
end
- node.children = child.children if child
+
+ check_for_no_children(child)
+ validate_and_append_child(parent, child, line, root)
end
- while has_children
- child, index = build_tree(index)
+ raise SyntaxError.new("Rules can't end in commas.", continued_rule.line) if continued_rule
- validate_and_append_child(node, child)
+ parent
+ end
- has_children = has_children?(index, tabs)
+ def validate_and_append_child(parent, child, line, root)
+ unless root
+ case child
+ when Tree::MixinDefNode
+ raise SyntaxError.new("Mixins may only be defined at the root of a document.", line.index)
+ when Tree::ImportNode
+ raise SyntaxError.new("Import directives may only be used at the root of a document.", line.index)
+ end
end
- return node, index
- end
-
- def validate_and_append_child(parent, child)
case child
- when :constant
- raise SyntaxError.new("Constants may only be declared at the root of a document.", @line)
- when :mixin
- raise SyntaxError.new("Mixins may only be defined at the root of a document.", @line)
when Array
- child.each do |c|
- if c.is_a?(Tree::DirectiveNode)
- raise SyntaxError.new("Import directives may only be used at the root of a document.", @line)
- end
- parent << c
- end
+ child.each {|c| validate_and_append_child(parent, c, line, root)}
when Tree::Node
parent << child
end
end
- def has_children?(index, tabs)
- next_line = ['//', 0]
- while !next_line.nil? && next_line[0] == '//' && next_line[1] = 0
- next_line = @lines[index]
- index += 1
- end
- next_line && next_line[1] > tabs
- end
+ def check_for_no_children(node)
+ return unless node.is_a?(Tree::RuleNode) && node.children.empty?
+ warning = (node.rules.size == 1) ? <<SHORT : <<LONG
+WARNING on line #{node.line}#{" of #{node.filename}" if node.filename}:
+Selector #{node.rules.first.inspect} doesn't have any properties and will not be rendered.
+SHORT
- def raw_next_line(index)
- [@lines[index][0], index + 1]
+WARNING on line #{node.line}#{" of #{node.filename}" if node.filename}:
+Selector
+ #{node.rules.join("\n ")}
+doesn't have any properties and will not be rendered.
+LONG
+
+ warn(warning.strip)
end
- def parse_line(line)
- case line[0]
- when ATTRIBUTE_CHAR
- if line[1] != ATTRIBUTE_CHAR
- parse_attribute(line, ATTRIBUTE)
- else
+ def parse_line(parent, line, root)
+ case line.text[0]
+ when PROPERTY_CHAR
+ if line.text[1] == PROPERTY_CHAR ||
+ (@options[:property_syntax] == :new &&
+ line.text =~ PROPERTY_OLD && $3.empty?)
# Support CSS3-style pseudo-elements,
- # which begin with ::
- Tree::RuleNode.new(line, @options[:style])
+ # which begin with ::,
+ # as well as pseudo-classes
+ # if we're using the new property syntax
+ Tree::RuleNode.new(line.text)
+ else
+ parse_property(line, PROPERTY_OLD)
end
- when Constant::CONSTANT_CHAR
- parse_constant(line)
+ when Script::VARIABLE_CHAR
+ parse_variable(line)
when COMMENT_CHAR
- parse_comment(line)
+ parse_comment(line.text)
when DIRECTIVE_CHAR
- parse_directive(line)
+ parse_directive(parent, line, root)
when ESCAPE_CHAR
- Tree::RuleNode.new(line[1..-1], @options[:style])
+ Tree::RuleNode.new(line.text[1..-1])
when MIXIN_DEFINITION_CHAR
parse_mixin_definition(line)
when MIXIN_INCLUDE_CHAR
- if line[1].nil? || line[1] == ?\s
- Tree::RuleNode.new(line, @options[:style])
+ if line.text[1].nil? || line.text[1] == ?\s
+ Tree::RuleNode.new(line.text)
else
- parse_mixin_include(line)
+ parse_mixin_include(line, root)
end
else
- if line =~ ATTRIBUTE_ALTERNATE_MATCHER
- parse_attribute(line, ATTRIBUTE_ALTERNATE)
+ if line.text =~ PROPERTY_NEW_MATCHER
+ parse_property(line, PROPERTY_NEW)
else
- Tree::RuleNode.new(line, @options[:style])
+ Tree::RuleNode.new(line.text)
end
end
end
- def parse_attribute(line, attribute_regx)
- if @options[:attribute_syntax] == :normal &&
- attribute_regx == ATTRIBUTE_ALTERNATE
- raise SyntaxError.new("Illegal attribute syntax: can't use alternate syntax when :attribute_syntax => :normal is set.")
- elsif @options[:attribute_syntax] == :alternate &&
- attribute_regx == ATTRIBUTE
- raise SyntaxError.new("Illegal attribute syntax: can't use normal syntax when :attribute_syntax => :alternate is set.")
- end
+ def parse_property(line, property_regx)
+ name, eq, value = line.text.scan(property_regx)[0]
- name, eq, value = line.scan(attribute_regx)[0]
-
if name.nil? || value.nil?
- raise SyntaxError.new("Invalid attribute: \"#{line}\".", @line)
+ raise SyntaxError.new("Invalid property: \"#{line.text}\".", @line)
end
-
- if eq.strip[0] == SCRIPT_CHAR
- value = Sass::Constant.parse(value, @constants, @line).to_s
+ expr = if (eq.strip[0] == SCRIPT_CHAR)
+ parse_script(value, :offset => line.offset + line.text.index(value))
+ else
+ value
end
-
- Tree::AttrNode.new(name, value, @options[:style])
+ Tree::PropNode.new(name, expr, property_regx == PROPERTY_OLD ? :old : :new)
end
- def parse_constant(line)
- name, op, value = line.scan(Sass::Constant::MATCH)[0]
- unless name && value
- raise SyntaxError.new("Invalid constant: \"#{line}\".", @line)
- end
+ def parse_variable(line)
+ name, op, value = line.text.scan(Script::MATCH)[0]
+ raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath variable declarations.", @line + 1) unless line.children.empty?
+ raise SyntaxError.new("Invalid variable: \"#{line.text}\".", @line) unless name && value
- constant = Sass::Constant.parse(value, @constants, @line)
- if op == '||='
- @constants[name] ||= constant
- else
- @constants[name] = constant
- end
-
- :constant
+ Tree::VariableNode.new(name, parse_script(value, :offset => line.offset + line.text.index(value)), op == '||=')
end
def parse_comment(line)
- if line[1] == SASS_COMMENT_CHAR
- :comment
- elsif line[1] == CSS_COMMENT_CHAR
- Tree::CommentNode.new(line, @options[:style])
+ if line[1] == CSS_COMMENT_CHAR || line[1] == SASS_COMMENT_CHAR
+ Tree::CommentNode.new(line, line[1] == SASS_COMMENT_CHAR)
else
- Tree::RuleNode.new(line, @options[:style])
+ Tree::RuleNode.new(line)
end
end
- def parse_directive(line)
- directive, value = line[1..-1].split(/\s+/, 2)
+ def parse_directive(parent, line, root)
+ directive, whitespace, value = line.text[1..-1].split(/(\s+)/, 2)
+ offset = directive.size + whitespace.size + 1 if whitespace
# If value begins with url( or ",
# it's a CSS @import rule and we don't want to touch it.
if directive == "import" && value !~ /^(url\(|")/
- import(value)
+ raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath import directives.", @line + 1) unless line.children.empty?
+ value.split(/,\s*/).map {|f| Tree::ImportNode.new(f)}
+ elsif directive == "for"
+ parse_for(line, root, value)
+ elsif directive == "else"
+ parse_else(parent, line, value)
+ elsif directive == "while"
+ raise SyntaxError.new("Invalid while directive '@while': expected expression.") unless value
+ Tree::WhileNode.new(parse_script(value, :offset => offset))
+ elsif directive == "if"
+ raise SyntaxError.new("Invalid if directive '@if': expected expression.") unless value
+ Tree::IfNode.new(parse_script(value, :offset => offset))
+ elsif directive == "debug"
+ raise SyntaxError.new("Invalid debug directive '@debug': expected expression.") unless value
+ raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath debug directives.", @line + 1) unless line.children.empty?
+ offset = line.offset + line.text.index(value).to_i
+ Tree::DebugNode.new(parse_script(value, :offset => offset))
else
- Tree::DirectiveNode.new(line, @options[:style])
+ Tree::DirectiveNode.new(line.text)
end
end
- def parse_mixin_definition(line)
- mixin_name = line[1..-1].strip
- @mixins[mixin_name] = []
- index = @line
- line, tabs = @lines[index]
- while !line.nil? && tabs > 0
- child, index = build_tree(index)
- validate_and_append_child(@mixins[mixin_name], child)
- line, tabs = @lines[index]
- end
- :mixin
- end
+ def parse_for(line, root, text)
+ var, from_expr, to_name, to_expr = text.scan(/^([^\s]+)\s+from\s+(.+)\s+(to|through)\s+(.+)$/).first
- def parse_mixin_include(line)
- mixin_name = line[1..-1]
- unless @mixins.has_key?(mixin_name)
- raise SyntaxError.new("Undefined mixin '#{mixin_name}'.", @line)
+ if var.nil? # scan failed, try to figure out why for error message
+ if text !~ /^[^\s]+/
+ expected = "variable name"
+ elsif text !~ /^[^\s]+\s+from\s+.+/
+ expected = "'from <expr>'"
+ else
+ expected = "'to <expr>' or 'through <expr>'"
+ end
+ raise SyntaxError.new("Invalid for directive '@for #{text}': expected #{expected}.", @line)
end
- @mixins[mixin_name]
+ raise SyntaxError.new("Invalid variable \"#{var}\".", @line) unless var =~ Script::VALIDATE
+
+ parsed_from = parse_script(from_expr, :offset => line.offset + line.text.index(from_expr))
+ parsed_to = parse_script(to_expr, :offset => line.offset + line.text.index(to_expr))
+ Tree::ForNode.new(var[1..-1], parsed_from, parsed_to, to_name == 'to')
end
- def import(files)
- nodes = []
+ def parse_else(parent, line, text)
+ previous = parent.last
+ raise SyntaxError.new("@else must come after @if.") unless previous.is_a?(Tree::IfNode)
- files.split(/,\s*/).each do |filename|
- engine = nil
-
- begin
- filename = self.class.find_file_to_import(filename, @options[:load_paths])
- rescue Exception => e
- raise SyntaxError.new(e.message, @line)
+ if text
+ if text !~ /^if\s+(.+)/
+ raise SyntaxError.new("Invalid else directive '@else #{text}': expected 'if <expr>'.", @line)
end
-
- if filename =~ /\.css$/
- nodes << Tree::DirectiveNode.new("@import url(#{filename})", @options[:style])
- else
- File.open(filename) do |file|
- new_options = @options.dup
- new_options[:filename] = filename
- engine = Sass::Engine.new(file.read, @options)
- end
-
- engine.constants.merge! @constants
- engine.mixins.merge! @mixins
-
- begin
- root = engine.render_to_tree
- rescue Sass::SyntaxError => err
- err.add_backtrace_entry(filename)
- raise err
- end
- root.children.each do |child|
- child.filename = filename
- nodes << child
- end
- @constants = engine.constants
- @mixins = engine.mixins
- end
+ expr = parse_script($1, :offset => line.offset + line.text.index($1))
end
- nodes
+ node = Tree::IfNode.new(expr)
+ append_children(node, line.children, false)
+ previous.add_else node
+ nil
end
- def self.find_file_to_import(filename, load_paths)
- was_sass = false
- original_filename = filename
+ def parse_mixin_definition(line)
+ name, arg_string = line.text.scan(/^=\s*([^(]+)(.*)$/).first
+ raise SyntaxError.new("Invalid mixin \"#{line.text[1..-1]}\".", @line) if name.nil?
- if filename[-5..-1] == ".sass"
- filename = filename[0...-5]
- was_sass = true
- elsif filename[-4..-1] == ".css"
- return filename
- end
+ offset = line.offset + line.text.size - arg_string.size
+ args = Script::Parser.new(arg_string.strip, @line, offset).parse_mixin_definition_arglist
+ default_arg_found = false
+ Tree::MixinDefNode.new(name, args)
+ end
- new_filename = find_full_path("#{filename}.sass", load_paths)
+ def parse_mixin_include(line, root)
+ name, arg_string = line.text.scan(/^\+\s*([^(]+)(.*)$/).first
+ raise SyntaxError.new("Invalid mixin include \"#{line.text}\".", @line) if name.nil?
- return new_filename if new_filename
- return filename + '.css' unless was_sass
- raise SyntaxError.new("File to import not found or unreadable: #{original_filename}.", @line)
+ offset = line.offset + line.text.size - arg_string.size
+ args = Script::Parser.new(arg_string.strip, @line, offset).parse_mixin_include_arglist
+ raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath mixin directives.", @line + 1) unless line.children.empty?
+ Tree::MixinNode.new(name, args)
end
- def self.find_full_path(filename, load_paths)
- load_paths.each do |path|
- ["_#{filename}", filename].each do |name|
- full_path = File.join(path, name)
- if File.readable?(full_path)
- return full_path
- end
- end
- end
- nil
+ def parse_script(script, options = {})
+ line = options[:line] || @line
+ offset = options[:offset] || 0
+ Script.parse(script, line, offset, @options[:filename])
end
end
end