haml 4.0.0 → 5.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (87) hide show
  1. checksums.yaml +7 -0
  2. data/.yardopts +1 -1
  3. data/CHANGELOG.md +117 -5
  4. data/FAQ.md +7 -17
  5. data/MIT-LICENSE +1 -1
  6. data/README.md +85 -42
  7. data/REFERENCE.md +181 -86
  8. data/Rakefile +47 -51
  9. data/lib/haml/attribute_builder.rb +163 -0
  10. data/lib/haml/attribute_compiler.rb +215 -0
  11. data/lib/haml/attribute_parser.rb +144 -0
  12. data/lib/haml/buffer.rb +38 -128
  13. data/lib/haml/compiler.rb +88 -295
  14. data/lib/haml/engine.rb +25 -41
  15. data/lib/haml/error.rb +3 -0
  16. data/lib/haml/escapable.rb +49 -0
  17. data/lib/haml/exec.rb +33 -19
  18. data/lib/haml/filters.rb +20 -24
  19. data/lib/haml/generator.rb +41 -0
  20. data/lib/haml/helpers/action_view_extensions.rb +3 -2
  21. data/lib/haml/helpers/action_view_mods.rb +44 -66
  22. data/lib/haml/helpers/action_view_xss_mods.rb +1 -0
  23. data/lib/haml/helpers/safe_erubi_template.rb +27 -0
  24. data/lib/haml/helpers/safe_erubis_template.rb +16 -4
  25. data/lib/haml/helpers/xss_mods.rb +18 -12
  26. data/lib/haml/helpers.rb +122 -58
  27. data/lib/haml/options.rb +39 -46
  28. data/lib/haml/parser.rb +278 -217
  29. data/lib/haml/{template/plugin.rb → plugin.rb} +8 -15
  30. data/lib/haml/railtie.rb +21 -11
  31. data/lib/haml/sass_rails_filter.rb +17 -4
  32. data/lib/haml/template/options.rb +12 -2
  33. data/lib/haml/template.rb +12 -6
  34. data/lib/haml/temple_engine.rb +120 -0
  35. data/lib/haml/temple_line_counter.rb +29 -0
  36. data/lib/haml/util.rb +80 -199
  37. data/lib/haml/version.rb +2 -1
  38. data/lib/haml.rb +2 -1
  39. data/test/attribute_parser_test.rb +101 -0
  40. data/test/engine_test.rb +306 -176
  41. data/test/filters_test.rb +32 -19
  42. data/test/gemfiles/Gemfile.rails-4.0.x +11 -0
  43. data/test/gemfiles/Gemfile.rails-4.0.x.lock +87 -0
  44. data/test/gemfiles/Gemfile.rails-4.1.x +5 -0
  45. data/test/gemfiles/Gemfile.rails-4.2.x +5 -0
  46. data/test/gemfiles/Gemfile.rails-5.0.x +4 -0
  47. data/test/helper_test.rb +282 -96
  48. data/test/options_test.rb +22 -0
  49. data/test/parser_test.rb +71 -4
  50. data/test/results/bemit.xhtml +4 -0
  51. data/test/results/eval_suppressed.xhtml +4 -4
  52. data/test/results/helpers.xhtml +43 -41
  53. data/test/results/helpful.xhtml +6 -3
  54. data/test/results/just_stuff.xhtml +21 -20
  55. data/test/results/list.xhtml +9 -9
  56. data/test/results/nuke_inner_whitespace.xhtml +22 -22
  57. data/test/results/nuke_outer_whitespace.xhtml +84 -92
  58. data/test/results/original_engine.xhtml +17 -17
  59. data/test/results/partial_layout.xhtml +4 -3
  60. data/test/results/partial_layout_erb.xhtml +4 -3
  61. data/test/results/partials.xhtml +11 -10
  62. data/test/results/silent_script.xhtml +63 -63
  63. data/test/results/standard.xhtml +156 -159
  64. data/test/results/tag_parsing.xhtml +19 -19
  65. data/test/results/very_basic.xhtml +2 -2
  66. data/test/results/whitespace_handling.xhtml +56 -50
  67. data/test/template_test.rb +44 -53
  68. data/test/template_test_helper.rb +38 -0
  69. data/test/templates/_text_area_helper.html.haml +4 -0
  70. data/test/templates/bemit.haml +3 -0
  71. data/test/templates/just_stuff.haml +1 -0
  72. data/test/templates/partial_layout_erb.erb +1 -1
  73. data/test/templates/standard_ugly.haml +1 -0
  74. data/test/templates/with_bom.haml +1 -0
  75. data/test/temple_line_counter_test.rb +40 -0
  76. data/test/test_helper.rb +26 -12
  77. data/test/util_test.rb +6 -47
  78. metadata +88 -106
  79. data/lib/haml/helpers/rails_323_textarea_fix.rb +0 -24
  80. data/test/gemfiles/Gemfile.rails-3.0.x +0 -5
  81. data/test/gemfiles/Gemfile.rails-3.1.x +0 -6
  82. data/test/gemfiles/Gemfile.rails-3.2.x +0 -5
  83. data/test/gemfiles/Gemfile.rails-master +0 -4
  84. data/test/templates/_av_partial_1_ugly.haml +0 -9
  85. data/test/templates/_av_partial_2_ugly.haml +0 -5
  86. data/test/templates/action_view_ugly.haml +0 -47
  87. data/test/templates/standard_ugly.haml +0 -43
@@ -0,0 +1,163 @@
1
+ # frozen_string_literal: true
2
+ module Haml
3
+ module AttributeBuilder
4
+ # https://html.spec.whatwg.org/multipage/syntax.html#attributes-2
5
+ INVALID_ATTRIBUTE_NAME_REGEX = /[ \0"'>\/=]/
6
+
7
+ class << self
8
+ def build_attributes(is_html, attr_wrapper, escape_attrs, hyphenate_data_attrs, attributes = {})
9
+ # @TODO this is an absolutely ridiculous amount of arguments. At least
10
+ # some of this needs to be moved into an instance method.
11
+ join_char = hyphenate_data_attrs ? '-' : '_'
12
+
13
+ attributes.each do |key, value|
14
+ if value.is_a?(Hash)
15
+ data_attributes = attributes.delete(key)
16
+ data_attributes = flatten_data_attributes(data_attributes, '', join_char)
17
+ data_attributes = build_data_keys(data_attributes, hyphenate_data_attrs, key)
18
+ verify_attribute_names!(data_attributes.keys)
19
+ attributes = data_attributes.merge(attributes)
20
+ end
21
+ end
22
+
23
+ result = attributes.collect do |attr, value|
24
+ next if value.nil?
25
+
26
+ value = filter_and_join(value, ' ') if attr == 'class'
27
+ value = filter_and_join(value, '_') if attr == 'id'
28
+
29
+ if value == true
30
+ next " #{attr}" if is_html
31
+ next " #{attr}=#{attr_wrapper}#{attr}#{attr_wrapper}"
32
+ elsif value == false
33
+ next
34
+ end
35
+
36
+ value =
37
+ if escape_attrs == :once
38
+ Haml::Helpers.escape_once(value.to_s)
39
+ elsif escape_attrs
40
+ Haml::Helpers.html_escape(value.to_s)
41
+ else
42
+ value.to_s
43
+ end
44
+ " #{attr}=#{attr_wrapper}#{value}#{attr_wrapper}"
45
+ end
46
+ result.compact!
47
+ result.sort!
48
+ result.join
49
+ end
50
+
51
+ # @return [String, nil]
52
+ def filter_and_join(value, separator)
53
+ return '' if (value.respond_to?(:empty?) && value.empty?)
54
+
55
+ if value.is_a?(Array)
56
+ value = value.flatten
57
+ value.map! {|item| item ? item.to_s : nil}
58
+ value.compact!
59
+ value = value.join(separator)
60
+ else
61
+ value = value ? value.to_s : nil
62
+ end
63
+ !value.nil? && !value.empty? && value
64
+ end
65
+
66
+ # Merges two attribute hashes.
67
+ # This is the same as `to.merge!(from)`,
68
+ # except that it merges id, class, and data attributes.
69
+ #
70
+ # ids are concatenated with `"_"`,
71
+ # and classes are concatenated with `" "`.
72
+ # data hashes are simply merged.
73
+ #
74
+ # Destructively modifies `to`.
75
+ #
76
+ # @param to [{String => String,Hash}] The attribute hash to merge into
77
+ # @param from [{String => Object}] The attribute hash to merge from
78
+ # @return [{String => String,Hash}] `to`, after being merged
79
+ def merge_attributes!(to, from)
80
+ from.keys.each do |key|
81
+ to[key] = merge_value(key, to[key], from[key])
82
+ end
83
+ to
84
+ end
85
+
86
+ # Merge multiple values to one attribute value. No destructive operation.
87
+ #
88
+ # @param key [String]
89
+ # @param values [Array<Object>]
90
+ # @return [String,Hash]
91
+ def merge_values(key, *values)
92
+ values.inject(nil) do |to, from|
93
+ merge_value(key, to, from)
94
+ end
95
+ end
96
+
97
+ def verify_attribute_names!(attribute_names)
98
+ attribute_names.each do |attribute_name|
99
+ if attribute_name =~ INVALID_ATTRIBUTE_NAME_REGEX
100
+ raise InvalidAttributeNameError.new("Invalid attribute name '#{attribute_name}' was rendered")
101
+ end
102
+ end
103
+ end
104
+
105
+ private
106
+
107
+ # Merge a couple of values to one attribute value. No destructive operation.
108
+ #
109
+ # @param to [String,Hash,nil]
110
+ # @param from [Object]
111
+ # @return [String,Hash]
112
+ def merge_value(key, to, from)
113
+ if from.kind_of?(Hash) || to.kind_of?(Hash)
114
+ from = { nil => from } if !from.is_a?(Hash)
115
+ to = { nil => to } if !to.is_a?(Hash)
116
+ to.merge(from)
117
+ elsif key == 'id'
118
+ merged_id = filter_and_join(from, '_')
119
+ if to && merged_id
120
+ merged_id = "#{to}_#{merged_id}"
121
+ elsif to || merged_id
122
+ merged_id ||= to
123
+ end
124
+ merged_id
125
+ elsif key == 'class'
126
+ merged_class = filter_and_join(from, ' ')
127
+ if to && merged_class
128
+ merged_class = (merged_class.split(' ') | to.split(' ')).sort.join(' ')
129
+ elsif to || merged_class
130
+ merged_class ||= to
131
+ end
132
+ merged_class
133
+ else
134
+ from
135
+ end
136
+ end
137
+
138
+ def build_data_keys(data_hash, hyphenate, attr_name="data")
139
+ Hash[data_hash.map do |name, value|
140
+ if name == nil
141
+ [attr_name, value]
142
+ elsif hyphenate
143
+ ["#{attr_name}-#{name.to_s.tr('_', '-')}", value]
144
+ else
145
+ ["#{attr_name}-#{name}", value]
146
+ end
147
+ end]
148
+ end
149
+
150
+ def flatten_data_attributes(data, key, join_char, seen = [])
151
+ return {key => data} unless data.is_a?(Hash)
152
+
153
+ return {key => nil} if seen.include? data.object_id
154
+ seen << data.object_id
155
+
156
+ data.sort {|x, y| x[0].to_s <=> y[0].to_s}.inject({}) do |hash, (k, v)|
157
+ joined = key == '' ? k : [key, k].join(join_char)
158
+ hash.merge! flatten_data_attributes(v, joined, join_char, seen)
159
+ end
160
+ end
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,215 @@
1
+ # frozen_string_literal: true
2
+ require 'haml/attribute_parser'
3
+
4
+ module Haml
5
+ class AttributeCompiler
6
+ # @param type [Symbol] :static or :dynamic
7
+ # @param key [String]
8
+ # @param value [String] Actual string value for :static type, value's Ruby literal for :dynamic type.
9
+ class AttributeValue < Struct.new(:type, :key, :value)
10
+ # @return [String] A Ruby literal of value.
11
+ def to_literal
12
+ case type
13
+ when :static
14
+ Haml::Util.inspect_obj(value)
15
+ when :dynamic
16
+ value
17
+ end
18
+ end
19
+
20
+ # Key's substring before a hyphen. This is necessary because values with the same
21
+ # base_key can conflict by Haml::AttributeBuidler#build_data_keys.
22
+ def base_key
23
+ key.split('-', 2).first
24
+ end
25
+ end
26
+
27
+ # Returns a script to render attributes on runtime.
28
+ #
29
+ # @param attributes [Hash]
30
+ # @param object_ref [String,:nil]
31
+ # @param dynamic_attributes [DynamicAttributes]
32
+ # @return [String] Attributes rendering code
33
+ def self.runtime_build(attributes, object_ref, dynamic_attributes)
34
+ "_hamlout.attributes(#{Haml::Util.inspect_obj(attributes)}, #{object_ref},#{dynamic_attributes.to_literal})"
35
+ end
36
+
37
+ # @param options [Haml::Options]
38
+ def initialize(options)
39
+ @is_html = [:html4, :html5].include?(options[:format])
40
+ @attr_wrapper = options[:attr_wrapper]
41
+ @escape_attrs = options[:escape_attrs]
42
+ @hyphenate_data_attrs = options[:hyphenate_data_attrs]
43
+ end
44
+
45
+ # Returns Temple expression to render attributes.
46
+ #
47
+ # @param attributes [Hash]
48
+ # @param object_ref [String,:nil]
49
+ # @param dynamic_attributes [DynamicAttributes]
50
+ # @return [Array] Temple expression
51
+ def compile(attributes, object_ref, dynamic_attributes)
52
+ if object_ref != :nil || !AttributeParser.available?
53
+ return [:dynamic, AttributeCompiler.runtime_build(attributes, object_ref, dynamic_attributes)]
54
+ end
55
+
56
+ parsed_hashes = [dynamic_attributes.new, dynamic_attributes.old].compact.map do |attribute_hash|
57
+ unless (hash = AttributeParser.parse(attribute_hash))
58
+ return [:dynamic, AttributeCompiler.runtime_build(attributes, object_ref, dynamic_attributes)]
59
+ end
60
+ hash
61
+ end
62
+ attribute_values = build_attribute_values(attributes, parsed_hashes)
63
+ AttributeBuilder.verify_attribute_names!(attribute_values.map(&:key))
64
+
65
+ values_by_base_key = attribute_values.group_by(&:base_key)
66
+ [:multi, *values_by_base_key.keys.sort.map { |base_key|
67
+ compile_attribute_values(values_by_base_key[base_key])
68
+ }]
69
+ end
70
+
71
+ private
72
+
73
+ # Returns array of AttributeValue instances from static attributes and dynamic_attributes. For each key,
74
+ # the values' order in returned value is preserved in the same order as Haml::Buffer#attributes's merge order.
75
+ #
76
+ # @param attributes [{ String => String }]
77
+ # @param parsed_hashes [{ String => String }]
78
+ # @return [Array<AttributeValue>]
79
+ def build_attribute_values(attributes, parsed_hashes)
80
+ [].tap do |attribute_values|
81
+ attributes.each do |key, static_value|
82
+ attribute_values << AttributeValue.new(:static, key, static_value)
83
+ end
84
+ parsed_hashes.each do |parsed_hash|
85
+ parsed_hash.each do |key, dynamic_value|
86
+ attribute_values << AttributeValue.new(:dynamic, key, dynamic_value)
87
+ end
88
+ end
89
+ end
90
+ end
91
+
92
+ # Compiles attribute values with the same base_key to Temple expression.
93
+ #
94
+ # @param values [Array<AttributeValue>] `base_key`'s results are the same. `key`'s result may differ.
95
+ # @return [Array] Temple expression
96
+ def compile_attribute_values(values)
97
+ if values.map(&:key).uniq.size == 1
98
+ compile_attribute(values.first.key, values)
99
+ else
100
+ runtime_build(values)
101
+ end
102
+ end
103
+
104
+ # @param values [Array<AttributeValue>]
105
+ # @return [Array] Temple expression
106
+ def runtime_build(values)
107
+ hash_content = values.group_by(&:key).map do |key, values_for_key|
108
+ "#{frozen_string(key)} => #{merged_value(key, values_for_key)}"
109
+ end.join(', ')
110
+ [:dynamic, "_hamlout.attributes({ #{hash_content} }, nil)"]
111
+ end
112
+
113
+ # Renders attribute values statically.
114
+ #
115
+ # @param values [Array<AttributeValue>]
116
+ # @return [Array] Temple expression
117
+ def static_build(values)
118
+ hash_content = values.group_by(&:key).map do |key, values_for_key|
119
+ "#{frozen_string(key)} => #{merged_value(key, values_for_key)}"
120
+ end.join(', ')
121
+
122
+ arguments = [@is_html, @attr_wrapper, @escape_attrs, @hyphenate_data_attrs]
123
+ code = "::Haml::AttributeBuilder.build_attributes"\
124
+ "(#{arguments.map { |a| Haml::Util.inspect_obj(a) }.join(', ')}, { #{hash_content} })"
125
+ [:static, eval(code).to_s]
126
+ end
127
+
128
+ # @param key [String]
129
+ # @param values [Array<AttributeValue>]
130
+ # @return [String]
131
+ def merged_value(key, values)
132
+ if values.size == 1
133
+ values.first.to_literal
134
+ else
135
+ "::Haml::AttributeBuilder.merge_values(#{frozen_string(key)}, #{values.map(&:to_literal).join(', ')})"
136
+ end
137
+ end
138
+
139
+ # @param str [String]
140
+ # @return [String]
141
+ def frozen_string(str)
142
+ "#{Haml::Util.inspect_obj(str)}.freeze"
143
+ end
144
+
145
+ # Compiles attribute values for one key to Temple expression that generates ` key='value'`.
146
+ #
147
+ # @param key [String]
148
+ # @param values [Array<AttributeValue>]
149
+ # @return [Array] Temple expression
150
+ def compile_attribute(key, values)
151
+ if values.all? { |v| Temple::StaticAnalyzer.static?(v.to_literal) }
152
+ return static_build(values)
153
+ end
154
+
155
+ case key
156
+ when 'id', 'class'
157
+ compile_id_or_class_attribute(key, values)
158
+ else
159
+ compile_common_attribute(key, values)
160
+ end
161
+ end
162
+
163
+ # @param id_or_class [String] "id" or "class"
164
+ # @param values [Array<AttributeValue>]
165
+ # @return [Array] Temple expression
166
+ def compile_id_or_class_attribute(id_or_class, values)
167
+ var = unique_name
168
+ [:multi,
169
+ [:code, "#{var} = (#{merged_value(id_or_class, values)})"],
170
+ [:case, var,
171
+ ['Hash, Array', runtime_build([AttributeValue.new(:dynamic, id_or_class, var)])],
172
+ ['false, nil', [:multi]],
173
+ [:else, [:multi,
174
+ [:static, " #{id_or_class}=#{@attr_wrapper}"],
175
+ [:escape, @escape_attrs, [:dynamic, var]],
176
+ [:static, @attr_wrapper]],
177
+ ]
178
+ ],
179
+ ]
180
+ end
181
+
182
+ # @param key [String] Not "id" or "class"
183
+ # @param values [Array<AttributeValue>]
184
+ # @return [Array] Temple expression
185
+ def compile_common_attribute(key, values)
186
+ var = unique_name
187
+ [:multi,
188
+ [:code, "#{var} = (#{merged_value(key, values)})"],
189
+ [:case, var,
190
+ ['Hash', runtime_build([AttributeValue.new(:dynamic, key, var)])],
191
+ ['true', true_value(key)],
192
+ ['false, nil', [:multi]],
193
+ [:else, [:multi,
194
+ [:static, " #{key}=#{@attr_wrapper}"],
195
+ [:escape, @escape_attrs, [:dynamic, var]],
196
+ [:static, @attr_wrapper]],
197
+ ]
198
+ ],
199
+ ]
200
+ end
201
+
202
+ def true_value(key)
203
+ if @is_html
204
+ [:static, " #{key}"]
205
+ else
206
+ [:static, " #{key}=#{@attr_wrapper}#{key}#{@attr_wrapper}"]
207
+ end
208
+ end
209
+
210
+ def unique_name
211
+ @unique_name ||= 0
212
+ "_haml_attribute_compiler#{@unique_name += 1}"
213
+ end
214
+ end
215
+ end
@@ -0,0 +1,144 @@
1
+ # frozen_string_literal: true
2
+ begin
3
+ require 'ripper'
4
+ rescue LoadError
5
+ end
6
+
7
+ module Haml
8
+ # Haml::AttriubuteParser parses Hash literal to { String (key name) => String (value literal) }.
9
+ module AttributeParser
10
+ class UnexpectedTokenError < StandardError; end
11
+ class UnexpectedKeyError < StandardError; end
12
+
13
+ # Indices in Ripper tokens
14
+ TYPE = 1
15
+ TEXT = 2
16
+
17
+ IGNORED_TYPES = %i[on_sp on_ignored_nl]
18
+
19
+ class << self
20
+ # @return [Boolean] - return true if AttributeParser.parse can be used.
21
+ def available?
22
+ defined?(Ripper) && Temple::StaticAnalyzer.available?
23
+ end
24
+
25
+ # @param [String] exp - Old attributes literal or Hash literal generated from new attributes.
26
+ # @return [Hash<String, String>,nil] - Return parsed attribute Hash whose values are Ruby literals, or return nil if argument is not a single Hash literal.
27
+ def parse(exp)
28
+ return nil unless hash_literal?(exp)
29
+
30
+ hash = {}
31
+ each_attribute(exp) do |key, value|
32
+ hash[key] = value
33
+ end
34
+ hash
35
+ rescue UnexpectedTokenError, UnexpectedKeyError
36
+ nil
37
+ end
38
+
39
+ private
40
+
41
+ # @param [String] exp - Ruby expression
42
+ # @return [Boolean] - Return true if exp is a single Hash literal
43
+ def hash_literal?(exp)
44
+ return false if Temple::StaticAnalyzer.syntax_error?(exp)
45
+ sym, body = Ripper.sexp(exp)
46
+ sym == :program && body.is_a?(Array) && body.size == 1 && body[0] && body[0][0] == :hash
47
+ end
48
+
49
+ # @param [Array] tokens - Ripper tokens. Scanned tokens will be destructively removed from this argument.
50
+ # @return [String] - attribute name in String
51
+ def shift_key!(tokens)
52
+ while !tokens.empty? && IGNORED_TYPES.include?(tokens.first[TYPE])
53
+ tokens.shift # ignore spaces
54
+ end
55
+
56
+ _, type, first_text = tokens.shift
57
+ case type
58
+ when :on_label # `key:`
59
+ first_text.tr(':', '')
60
+ when :on_symbeg # `:key =>`, `:'key' =>` or `:"key" =>`
61
+ key = tokens.shift[TEXT]
62
+ if first_text != ':' # `:'key'` or `:"key"`
63
+ expect_string_end!(tokens.shift)
64
+ end
65
+ shift_hash_rocket!(tokens)
66
+ key
67
+ when :on_tstring_beg # `"key":`, `'key':` or `"key" =>`
68
+ key = tokens.shift[TEXT]
69
+ next_token = tokens.shift
70
+ if next_token[TYPE] != :on_label_end # on_label_end is `":` or `':`, so `"key" =>`
71
+ expect_string_end!(next_token)
72
+ shift_hash_rocket!(tokens)
73
+ end
74
+ key
75
+ else
76
+ raise UnexpectedKeyError.new("unexpected token is given!: #{first_text} (#{type})")
77
+ end
78
+ end
79
+
80
+ # @param [Array] token - Ripper token
81
+ def expect_string_end!(token)
82
+ if token[TYPE] != :on_tstring_end
83
+ raise UnexpectedTokenError
84
+ end
85
+ end
86
+
87
+ # @param [Array] tokens - Ripper tokens
88
+ def shift_hash_rocket!(tokens)
89
+ until tokens.empty?
90
+ _, type, str = tokens.shift
91
+ break if type == :on_op && str == '=>'
92
+ end
93
+ end
94
+
95
+ # @param [String] hash_literal
96
+ # @param [Proc] block - that takes [String, String] as arguments
97
+ def each_attribute(hash_literal, &block)
98
+ all_tokens = Ripper.lex(hash_literal.strip)
99
+ all_tokens = all_tokens[1...-1] || [] # strip tokens for brackets
100
+
101
+ each_balaned_tokens(all_tokens) do |tokens|
102
+ key = shift_key!(tokens)
103
+ value = tokens.map(&:last).join.strip
104
+ block.call(key, value)
105
+ end
106
+ end
107
+
108
+ # @param [Array] tokens - Ripper tokens
109
+ # @param [Proc] block - that takes balanced Ripper tokens as arguments
110
+ def each_balaned_tokens(tokens, &block)
111
+ attr_tokens = []
112
+ open_tokens = Hash.new { |h, k| h[k] = 0 }
113
+
114
+ tokens.each do |token|
115
+ case token[TYPE]
116
+ when :on_comma
117
+ if open_tokens.values.all?(&:zero?)
118
+ block.call(attr_tokens)
119
+ attr_tokens = []
120
+ next
121
+ end
122
+ when :on_lbracket
123
+ open_tokens[:array] += 1
124
+ when :on_rbracket
125
+ open_tokens[:array] -= 1
126
+ when :on_lbrace
127
+ open_tokens[:block] += 1
128
+ when :on_rbrace
129
+ open_tokens[:block] -= 1
130
+ when :on_lparen
131
+ open_tokens[:paren] += 1
132
+ when :on_rparen
133
+ open_tokens[:paren] -= 1
134
+ when *IGNORED_TYPES
135
+ next if attr_tokens.empty?
136
+ end
137
+
138
+ attr_tokens << token
139
+ end
140
+ block.call(attr_tokens) unless attr_tokens.empty?
141
+ end
142
+ end
143
+ end
144
+ end