From 000b6657b5a9fbc18e64ba75edfd2a2a2c8e8bc2 Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Mon, 19 Oct 2020 20:17:19 -0700 Subject: [PATCH] Implement multiline attributes Close #981 --- lib/haml/parser.rb | 34 +++++++++++++++++++++++++++++++--- test/cases/attribute_test.rb | 15 +++++++++++++++ test/cases/exception_test.rb | 6 +++--- 3 files changed, 49 insertions(+), 6 deletions(-) diff --git a/lib/haml/parser.rb b/lib/haml/parser.rb index 5cacf3f6..051ee6ab 100644 --- a/lib/haml/parser.rb +++ b/lib/haml/parser.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +require 'ripper' require 'strscan' module Haml @@ -90,6 +91,9 @@ module Haml ID_KEY = 'id'.freeze CLASS_KEY = 'class'.freeze + # Used for scanning old attributes, substituting the first '{' + METHOD_CALL_PREFIX = 'a(' + def initialize(options) @options = Options.wrap(options) # Record the indent levels of "if" statements to validate the subsequent @@ -651,13 +655,18 @@ module Haml # @return [String] rest # @return [Integer] last_line def parse_old_attributes(text) - text = text.dup last_line = @line.index + 1 begin - attributes_hash, rest = balance(text, ?{, ?}) + # Old attributes often look like a valid Hash literal, but it sometimes allow code like + # `{ hash, foo: bar }`, which is compiled to `_hamlout.attributes({}, nil, hash, foo: bar)`. + # + # To scan such code correctly, this scans `a( hash, foo: bar }` instead, stops when there is + # 1 more :on_embexpr_end (the last '}') than :on_embexpr_beg, and resurrects '{' afterwards. + balanced, rest = balance_tokens(text.sub(?{, METHOD_CALL_PREFIX), :on_embexpr_beg, :on_embexpr_end, count: 1) + attributes_hash = balanced.sub(METHOD_CALL_PREFIX, ?{) rescue SyntaxError => e - if text.strip[-1] == ?, && e.message == Error.message(:unbalanced_brackets) + if e.message == Error.message(:unbalanced_brackets) && !@template.empty? text << "\n#{@next_line.text}" last_line += 1 next_line @@ -811,6 +820,25 @@ module Haml Haml::Util.balance(*args) or raise(SyntaxError.new(Error.message(:unbalanced_brackets))) end + # Unlike #balance, this balances Ripper tokens to balance something like `{ a: "}" }` correctly. + def balance_tokens(buf, start, finish, count: 0) + text = ''.dup + Ripper.lex(buf).each do |_, token, str| + text << str + case token + when start + count += 1 + when finish + count -= 1 + end + + if count == 0 + return text, buf.sub(text, '') + end + end + raise SyntaxError.new(Error.message(:unbalanced_brackets)) + end + def block_opened? @next_line.tabs > @line.tabs end diff --git a/test/cases/attribute_test.rb b/test/cases/attribute_test.rb index 36889f39..f2d74428 100644 --- a/test/cases/attribute_test.rb +++ b/test/cases/attribute_test.rb @@ -109,4 +109,19 @@ HAML %a{h1, :aria => h2} HAML end + + def test_multiline_attributes + assert_equal(<Haml +HTML +.haml#info{ + "data": { + "content": "/:|}", + "haml-info": { + "url": "https://haml.info", + } + } +} Haml +HAML + end end \ No newline at end of file diff --git a/test/cases/exception_test.rb b/test/cases/exception_test.rb index 01194a38..cbf69b42 100644 --- a/test/cases/exception_test.rb +++ b/test/cases/exception_test.rb @@ -56,9 +56,9 @@ class ExceptionTest < TestBase "%p{'foo' => 'bar' 'bar' => 'baz'}" => :compile, "%p{:foo => }" => :compile, "%p{=> 'bar'}" => :compile, - "%p{'foo => 'bar'}" => :compile, - "%p{:foo => 'bar}" => :compile, - "%p{:foo => 'bar\"}" => :compile, + "%p{'foo => 'bar'}" => error(:unbalanced_brackets), + "%p{:foo => 'bar}" => error(:unbalanced_brackets), + "%p{:foo => 'bar\"}" => error(:unbalanced_brackets), # Regression tests "foo\n\n\n bar" => [error(:illegal_nesting_plain), 4], "%p/\n\n bar" => [error(:illegal_nesting_self_closing), 3],