[Sass] Parse mixin definitions and includes using the SassScript parser.

Closes gh-20
This commit is contained in:
Nathan Weizenbaum 2009-07-19 14:20:55 -07:00
parent 678bceee9a
commit 350d42be41
6 changed files with 87 additions and 43 deletions

View File

@ -425,46 +425,24 @@ END
nil
end
# parses out the arguments between the commas and cleans up the mixin arguments
# returns nil if it fails to parse, otherwise an array.
def parse_mixin_arguments(arg_string)
arg_string = arg_string.strip
return [] if arg_string.empty?
return nil unless (arg_string[0] == ?( && arg_string[-1] == ?))
arg_string = arg_string[1...-1]
arg_string.split(",", -1).map {|a| a.strip}
end
def parse_mixin_definition(line)
name, arg_string = line.text.scan(/^=\s*([^(]+)(.*)$/).first
args = parse_mixin_arguments(arg_string)
raise SyntaxError.new("Invalid mixin \"#{line.text[1..-1]}\".", @line) if name.nil? || args.nil?
raise SyntaxError.new("Invalid mixin \"#{line.text[1..-1]}\".", @line) if name.nil?
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
required_arg_count = 0
args.map! do |arg|
raise SyntaxError.new("Mixin arguments can't be empty.", @line) if arg.empty? || arg == "!"
unless arg[0] == Script::VARIABLE_CHAR
raise SyntaxError.new("Mixin argument \"#{arg}\" must begin with an exclamation point (!).", @line)
end
arg, default = arg.split(/\s*=\s*/, 2)
required_arg_count += 1 unless default
default_arg_found ||= default
raise SyntaxError.new("Invalid variable \"#{arg}\".", @line) unless arg =~ Script::VALIDATE
raise SyntaxError.new("Required arguments must not follow optional arguments \"#{arg}\".", @line) if default_arg_found && !default
default = parse_script(default, :offset => line.offset + line.text.index(default)) if default
[arg[1..-1], default]
end
Tree::MixinDefNode.new(name, args)
end
def parse_mixin_include(line, root)
name, arg_string = line.text.scan(/^\+\s*([^(]+)(.*)$/).first
args = parse_mixin_arguments(arg_string)
raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath mixin directives.", @line + 1) unless line.children.empty?
raise SyntaxError.new("Invalid mixin include \"#{line.text}\".", @line) if name.nil? || args.nil?
args.each {|a| raise SyntaxError.new("Mixin arguments can't be empty.", @line) if a.empty?}
raise SyntaxError.new("Invalid mixin include \"#{line.text}\".", @line) if name.nil?
Tree::MixinNode.new(name, args.map {|s| parse_script(s, :offset => line.offset + line.text.index(s))})
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 parse_script(script, options = {})

View File

@ -28,6 +28,7 @@ module Sass
'*' => :times,
'/' => :div,
'%' => :mod,
'=' => :single_eq,
'(' => :lparen,
')' => :rparen,
',' => :comma,

View File

@ -36,10 +36,42 @@ module Sass
# @raise [Sass::SyntaxError] if the expression isn't valid SassScript
def parse
expr = assert_expr :expr
raise Sass::SyntaxError.new("Unexpected #{@lexer.peek.type} token.") unless @lexer.done?
assert_done
expr
end
# Parses the argument list for a mixin include.
#
# @return [Array<Script::Node>] The root nodes of the arguments.
# @raise [Sass::SyntaxError] if the argument list isn't valid SassScript
def parse_mixin_include_arglist
args = []
if try_tok(:lparen)
args = arglist || args
assert_tok(:rparen)
end
assert_done
args
end
# Parses the argument list for a mixin definition.
#
# @return [Array<Script::Node>] The root nodes of the arguments.
# @raise [Sass::SyntaxError] if the argument list isn't valid SassScript
def parse_mixin_definition_arglist
args = []
if try_tok(:lparen)
args = defn_arglist(false) || args
assert_tok(:rparen)
end
assert_done
args
end
# Parses a SassScript expression.
#
# @overload parse(str, line, offset, filename = nil)
@ -120,6 +152,19 @@ END
end
end
def defn_arglist(must_have_default)
return unless c = try_tok(:const)
var = Script::Variable.new(c.value)
if try_tok(:single_eq)
val = assert_expr(:concat)
elsif must_have_default
raise SyntaxError.new("Required argument #{var.inspect} must come before any optional arguments.", @line)
end
return [[var, val]] unless try_tok(:comma)
[[var, val], *defn_arglist(val)]
end
def arglist
return unless e = concat
return [e] unless try_tok(:comma)
@ -167,6 +212,11 @@ END
peeked = @lexer.peek
peeked && names.include?(peeked.type) && @lexer.next
end
def assert_done
return if @lexer.done?
raise Sass::SyntaxError.new("Unexpected #{@lexer.peek.type} token.")
end
end
end
end

View File

@ -5,8 +5,8 @@ module Sass
# @see Sass::Tree
class MixinDefNode < Node
# @param name [String] The mixin name
# @param args [Array<(String, Script::Node)>] The arguments for the mixin.
# Each element is a tuple containing the name of the argument
# @param args [Array<(Script::Node, Script::Node)>] The arguments for the mixin.
# Each element is a tuple containing the variable for argument
# and the parse tree for the default value of the argument
def initialize(name, args)
@name = name

View File

@ -32,14 +32,14 @@ Mixin #{@name} takes #{mixin.args.size} argument#{'s' if mixin.args.size != 1}
END
environment = mixin.args.zip(@args).
inject(Sass::Environment.new(mixin.environment)) do |env, ((name, default), value)|
env.set_local_var(name,
inject(Sass::Environment.new(mixin.environment)) do |env, ((var, default), value)|
env.set_local_var(var.name,
if value
value.perform(environment)
elsif default
default.perform(env)
end)
raise Sass::SyntaxError.new("Mixin #{@name} is missing parameter !#{name}.") unless env.var(name)
raise Sass::SyntaxError.new("Mixin #{@name} is missing parameter #{var.inspect}.") unless env.var(var.name)
env
end
mixin.tree.map {|c| c.perform(environment)}.flatten

View File

@ -65,14 +65,14 @@ class SassEngineTest < Test::Unit::TestCase
"a\n b: c\na\n d: e" => ["The line was indented 2 levels deeper than the previous line.", 4],
"a\n b: c\n a\n d: e" => ["The line was indented 3 levels deeper than the previous line.", 4],
"a\n \tb: c" => ["Indentation can't use both tabs and spaces.", 2],
"=a(" => 'Invalid mixin "a(".',
"=a(b)" => 'Mixin argument "b" must begin with an exclamation point (!).',
"=a(,)" => "Mixin arguments can't be empty.",
"=a(!)" => "Mixin arguments can't be empty.",
"=a(!foo bar)" => "Invalid variable \"!foo bar\".",
"=a(" => 'Expected rparen token, was end of text.',
"=a(b)" => 'Expected rparen token, was ident token.',
"=a(,)" => "Expected rparen token, was comma token.",
"=a(!)" => "Syntax error in '(!)' at character 4.",
"=a(!foo bar)" => "Expected rparen token, was ident token.",
"=foo\n bar: baz\n+foo" => ["Properties aren't allowed at the root of a document.", 2],
"a-\#{!b\n c: d" => ["Expected end_interpolation token, was end of text.", 1],
"=a(!b = 1, !c)" => "Required arguments must not follow optional arguments \"!c\".",
"=a(!b = 1, !c)" => "Required argument !c must come before any optional arguments.",
"=a(!b = 1)\n :a= !b\ndiv\n +a(1,2)" => "Mixin a takes 1 argument but 2 were passed.",
"=a(!b)\n :a= !b\ndiv\n +a" => "Mixin a is missing parameter !b.",
"@else\n a\n b: c" => ["@else must come after @if.", 1],
@ -706,6 +706,21 @@ SASS
# Regression tests
def test_parens_in_mixins
assert_equal(<<CSS, render(<<SASS))
.foo {
color: #01ff7f;
background-color: #000102; }
CSS
=foo(!c1, !c2 = rgb(0, 1, 2))
color = !c1
background-color = !c2
.foo
+foo(rgb(1,255,127))
SASS
end
def test_comment_beneath_prop
assert_equal(<<RESULT, render(<<SOURCE))
.box {