diff --git a/lib/haml.rb b/lib/haml.rb index 95fe34d1..8e952fae 100644 --- a/lib/haml.rb +++ b/lib/haml.rb @@ -133,16 +133,23 @@ $LOAD_PATH << dir unless $LOAD_PATH.include?(dir) # The hash is placed after the tag is defined. # For example: # -# %head{ :name => "doc_head" } -# %script{ 'type' => "text/" + "javascript", -# :src => "javascripts/script_#{2 + 7}" } +# %html{:xmlns => "http://www.w3.org/1999/xhtml", "xml:lang" => "en", :lang => "en"} # # is compiled to: # -# -# -# +# +# +# Attribute hashes can also be stretched out over multiple lines +# to accomidate many attributes. +# However, newlines may only be placed immediately after commas. +# For example: +# +# %script{:type => "text/javascript", +# :src => "javascripts/script_#{2 + 7}"} +# +# is compiled to: +# +# # # ===== Attribute Methods # diff --git a/lib/haml/precompiler.rb b/lib/haml/precompiler.rb index 162335fb..5a56f731 100644 --- a/lib/haml/precompiler.rb +++ b/lib/haml/precompiler.rb @@ -409,6 +409,7 @@ END attributes[key] = value end + text.count("\n").times { newline } attributes end @@ -452,10 +453,10 @@ END def parse_tag(line) raise SyntaxError.new("Invalid tag: \"#{line}\".") unless match = line.scan(/%([-:\w]+)([-\w\.\#]*)(.*)/)[0] tag_name, attributes, rest = match - attributes_hash, rest = parse_attributes(rest) if rest[0] == ?{ + attributes_hash, rest, last_line = parse_attributes(rest) if rest[0] == ?{ if rest object_ref, rest = balance(rest, ?[, ?]) if rest[0] == ?[ - attributes_hash, rest = parse_attributes(rest) if rest[0] == ?{ && attributes_hash.nil? + attributes_hash, rest, last_line = parse_attributes(rest) if rest[0] == ?{ && attributes_hash.nil? nuke_whitespace, action, value = rest.scan(/(<>|><|[><])?([=\/\~&!])?(.*)?/)[0] nuke_whitespace ||= '' nuke_outer_whitespace = nuke_whitespace.include? '>' @@ -463,21 +464,36 @@ END end value = value.to_s.strip [tag_name, attributes, attributes_hash, object_ref, nuke_outer_whitespace, - nuke_inner_whitespace, action, value] + nuke_inner_whitespace, action, value, last_line || @index] end def parse_attributes(line) - scanner = StringScanner.new(line) - attributes_hash, rest = balance(scanner, ?{, ?}) + line = line.dup + last_line = @index + + begin + attributes_hash, rest = balance(line, ?{, ?}) + rescue SyntaxError => e + if line.strip[-1] == ?, && e.message == "Unbalanced brackets." + line << "\n" << @next_line.text + last_line += 1 + next_line + @block_opened = @next_line.tabs > @line.tabs && !@next_line.text.empty? + retry + end + + raise e + end + attributes_hash = attributes_hash[1...-1] if attributes_hash - return attributes_hash, rest + return attributes_hash, rest, last_line end # Parses a line that will render as an XHTML tag, and adds the code that will # render that tag to @precompiled. def render_tag(line) tag_name, attributes, attributes_hash, object_ref, nuke_outer_whitespace, - nuke_inner_whitespace, action, value = parse_tag(line) + nuke_inner_whitespace, action, value, last_line = parse_tag(line) raise SyntaxError.new("Illegal element: classes and ids must have values.") if attributes =~ /[\.#](\.|#|\z)/ @@ -517,8 +533,8 @@ END raise SyntaxError.new("Illegal nesting: nesting within a self-closing tag is illegal.", @next_line.index) if @block_opened && self_closing raise SyntaxError.new("Illegal nesting: content can't be both given on the same line as %#{tag_name} and nested within it.", @next_line.index) if @block_opened && !value.empty? - raise SyntaxError.new("There's no Ruby code for #{action} to evaluate.") if parse && value.empty? - raise SyntaxError.new("Self-closing tags can't have content.") if self_closing && !value.empty? + raise SyntaxError.new("There's no Ruby code for #{action} to evaluate.", last_line - 1) if parse && value.empty? + raise SyntaxError.new("Self-closing tags can't have content.", last_line - 1) if self_closing && !value.empty? self_closing ||= !!( !@block_opened && value.empty? && @options[:autoclose].include?(tag_name) ) @@ -738,7 +754,7 @@ END def balance(scanner, start, finish, count = 0) str = '' scanner = StringScanner.new(scanner) unless scanner.is_a? StringScanner - regexp = Regexp.new("(.*?)[\\#{start.chr}\\#{finish.chr}]") + regexp = Regexp.new("(.*?)[\\#{start.chr}\\#{finish.chr}]", Regexp::MULTILINE) while scanner.scan(regexp) str << scanner.matched count += 1 if scanner.matched[-1] == start diff --git a/test/haml/engine_test.rb b/test/haml/engine_test.rb index fe21842e..65fb0866 100644 --- a/test/haml/engine_test.rb +++ b/test/haml/engine_test.rb @@ -32,6 +32,13 @@ END ".= a" => "Illegal element: classes and ids must have values.", "%p..a" => "Illegal element: classes and ids must have values.", "%a/ b" => "Self-closing tags can't have content.", + "%p{:a => 'b',\n:c => 'd'}/ e" => ["Self-closing tags can't have content.", 2], + "%p{:a => 'b',\n:c => 'd'}=" => ["There's no Ruby code for = to evaluate.", 2], + "%p.{:a => 'b',\n:c => 'd'} e" => ["Illegal element: classes and ids must have values.", 1], + "%p{:a => 'b',\n:c => 'd',\n:e => 'f'}\n%p/ a" => ["Self-closing tags can't have content.", 4], + "%p{:a => 'b',\n:c => 'd',\n:e => 'f'}\n- raise 'foo'" => ["foo", 4], + "%p{:a => 'b',\n:c => raise('foo'),\n:e => 'f'}" => ["foo", 2], + "%p{:a => 'b',\n:c => 'd',\n:e => raise('foo')}" => ["foo", 3], # Regression tests "- raise 'foo'\n\n\n\nbar" => ["foo", 1], @@ -121,6 +128,13 @@ END assert_equal("\n", render("%img{:width => nil, :src => '/foo.png', :alt => String.new}")) end + def test_attribute_hash_with_newlines + assert_equal("

foop

\n", render("%p{:a => 'b',\n :c => 'd'} foop")) + assert_equal("

\n foop\n

\n", render("%p{:a => 'b',\n :c => 'd'}\n foop")) + assert_equal("

\n", render("%p{:a => 'b',\n :c => 'd'}/")) + assert_equal("

\n", render("%p{:a => 'b',\n :c => 'd',\n :e => 'f'}")) + end + def test_end_of_file_multiline assert_equal("

0

\n

1

\n

2

\n", render("- for i in (0...3)\n %p= |\n i |")) end