[Sass] Parse mixin definitions and includes using the SassScript parser.
Closes gh-20
This commit is contained in:
parent
678bceee9a
commit
350d42be41
|
@ -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 = {})
|
||||
|
|
|
@ -28,6 +28,7 @@ module Sass
|
|||
'*' => :times,
|
||||
'/' => :div,
|
||||
'%' => :mod,
|
||||
'=' => :single_eq,
|
||||
'(' => :lparen,
|
||||
')' => :rparen,
|
||||
',' => :comma,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue