From 350d42be419f2a6d8b86472239c5e1992f41bded Mon Sep 17 00:00:00 2001 From: Nathan Weizenbaum Date: Sun, 19 Jul 2009 14:20:55 -0700 Subject: [PATCH] [Sass] Parse mixin definitions and includes using the SassScript parser. Closes gh-20 --- lib/sass/engine.rb | 40 ++++++------------------- lib/sass/script/lexer.rb | 1 + lib/sass/script/parser.rb | 52 ++++++++++++++++++++++++++++++++- lib/sass/tree/mixin_def_node.rb | 4 +-- lib/sass/tree/mixin_node.rb | 6 ++-- test/sass/engine_test.rb | 27 +++++++++++++---- 6 files changed, 87 insertions(+), 43 deletions(-) diff --git a/lib/sass/engine.rb b/lib/sass/engine.rb index 09357c7e..52b93115 100644 --- a/lib/sass/engine.rb +++ b/lib/sass/engine.rb @@ -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 = {}) diff --git a/lib/sass/script/lexer.rb b/lib/sass/script/lexer.rb index 70f93621..86e7c3b8 100644 --- a/lib/sass/script/lexer.rb +++ b/lib/sass/script/lexer.rb @@ -28,6 +28,7 @@ module Sass '*' => :times, '/' => :div, '%' => :mod, + '=' => :single_eq, '(' => :lparen, ')' => :rparen, ',' => :comma, diff --git a/lib/sass/script/parser.rb b/lib/sass/script/parser.rb index f94cc126..bb6b3dcf 100644 --- a/lib/sass/script/parser.rb +++ b/lib/sass/script/parser.rb @@ -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] 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] 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 diff --git a/lib/sass/tree/mixin_def_node.rb b/lib/sass/tree/mixin_def_node.rb index d2c24364..77d0a2a4 100644 --- a/lib/sass/tree/mixin_def_node.rb +++ b/lib/sass/tree/mixin_def_node.rb @@ -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 diff --git a/lib/sass/tree/mixin_node.rb b/lib/sass/tree/mixin_node.rb index 0cc77a29..fea53911 100644 --- a/lib/sass/tree/mixin_node.rb +++ b/lib/sass/tree/mixin_node.rb @@ -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 diff --git a/test/sass/engine_test.rb b/test/sass/engine_test.rb index a8a81ae1..d1ece2a7 100755 --- a/test/sass/engine_test.rb +++ b/test/sass/engine_test.rb @@ -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(<