haml 4.0.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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