haml 4.0.7 → 5.0.4

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 (123) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +18 -0
  3. data/.gitmodules +3 -0
  4. data/.travis.yml +54 -0
  5. data/.yardopts +1 -1
  6. data/CHANGELOG.md +96 -4
  7. data/FAQ.md +4 -14
  8. data/Gemfile +19 -0
  9. data/MIT-LICENSE +1 -1
  10. data/README.md +80 -42
  11. data/REFERENCE.md +116 -64
  12. data/Rakefile +46 -54
  13. data/TODO +24 -0
  14. data/benchmark.rb +66 -0
  15. data/haml.gemspec +38 -0
  16. data/lib/haml/.gitattributes +1 -0
  17. data/lib/haml/attribute_builder.rb +163 -0
  18. data/lib/haml/attribute_compiler.rb +223 -0
  19. data/lib/haml/attribute_parser.rb +148 -0
  20. data/lib/haml/buffer.rb +22 -132
  21. data/lib/haml/compiler.rb +89 -298
  22. data/lib/haml/engine.rb +25 -41
  23. data/lib/haml/error.rb +3 -0
  24. data/lib/haml/escapable.rb +49 -0
  25. data/lib/haml/exec.rb +38 -19
  26. data/lib/haml/filters.rb +18 -24
  27. data/lib/haml/generator.rb +41 -0
  28. data/lib/haml/helpers/action_view_extensions.rb +3 -2
  29. data/lib/haml/helpers/action_view_mods.rb +42 -60
  30. data/lib/haml/helpers/action_view_xss_mods.rb +1 -0
  31. data/lib/haml/helpers/safe_erubi_template.rb +19 -0
  32. data/lib/haml/helpers/safe_erubis_template.rb +4 -1
  33. data/lib/haml/helpers/xss_mods.rb +18 -12
  34. data/lib/haml/helpers.rb +132 -89
  35. data/lib/haml/options.rb +41 -47
  36. data/lib/haml/parser.rb +278 -216
  37. data/lib/haml/{template/plugin.rb → plugin.rb} +8 -15
  38. data/lib/haml/railtie.rb +38 -12
  39. data/lib/haml/sass_rails_filter.rb +17 -4
  40. data/lib/haml/template/options.rb +12 -2
  41. data/lib/haml/template.rb +12 -6
  42. data/lib/haml/temple_engine.rb +121 -0
  43. data/lib/haml/temple_line_counter.rb +29 -0
  44. data/lib/haml/util.rb +80 -199
  45. data/lib/haml/version.rb +2 -1
  46. data/lib/haml.rb +1 -0
  47. data/yard/default/.gitignore +1 -0
  48. data/yard/default/fulldoc/html/css/common.sass +15 -0
  49. data/yard/default/layout/html/footer.erb +12 -0
  50. metadata +50 -111
  51. data/test/engine_test.rb +0 -2013
  52. data/test/erb/_av_partial_1.erb +0 -12
  53. data/test/erb/_av_partial_2.erb +0 -8
  54. data/test/erb/action_view.erb +0 -62
  55. data/test/erb/standard.erb +0 -55
  56. data/test/filters_test.rb +0 -254
  57. data/test/gemfiles/Gemfile.rails-3.0.x +0 -5
  58. data/test/gemfiles/Gemfile.rails-3.1.x +0 -6
  59. data/test/gemfiles/Gemfile.rails-3.2.x +0 -5
  60. data/test/gemfiles/Gemfile.rails-4.0.x +0 -5
  61. data/test/haml-spec/LICENSE +0 -14
  62. data/test/haml-spec/README.md +0 -106
  63. data/test/haml-spec/lua_haml_spec.lua +0 -38
  64. data/test/haml-spec/perl_haml_test.pl +0 -81
  65. data/test/haml-spec/ruby_haml_test.rb +0 -23
  66. data/test/haml-spec/tests.json +0 -660
  67. data/test/helper_test.rb +0 -583
  68. data/test/markaby/standard.mab +0 -52
  69. data/test/mocks/article.rb +0 -6
  70. data/test/parser_test.rb +0 -105
  71. data/test/results/content_for_layout.xhtml +0 -12
  72. data/test/results/eval_suppressed.xhtml +0 -9
  73. data/test/results/helpers.xhtml +0 -70
  74. data/test/results/helpful.xhtml +0 -10
  75. data/test/results/just_stuff.xhtml +0 -70
  76. data/test/results/list.xhtml +0 -12
  77. data/test/results/nuke_inner_whitespace.xhtml +0 -40
  78. data/test/results/nuke_outer_whitespace.xhtml +0 -148
  79. data/test/results/original_engine.xhtml +0 -20
  80. data/test/results/partial_layout.xhtml +0 -5
  81. data/test/results/partial_layout_erb.xhtml +0 -5
  82. data/test/results/partials.xhtml +0 -21
  83. data/test/results/render_layout.xhtml +0 -3
  84. data/test/results/silent_script.xhtml +0 -74
  85. data/test/results/standard.xhtml +0 -162
  86. data/test/results/tag_parsing.xhtml +0 -23
  87. data/test/results/very_basic.xhtml +0 -5
  88. data/test/results/whitespace_handling.xhtml +0 -90
  89. data/test/template_test.rb +0 -354
  90. data/test/templates/_av_partial_1.haml +0 -9
  91. data/test/templates/_av_partial_1_ugly.haml +0 -9
  92. data/test/templates/_av_partial_2.haml +0 -5
  93. data/test/templates/_av_partial_2_ugly.haml +0 -5
  94. data/test/templates/_layout.erb +0 -3
  95. data/test/templates/_layout_for_partial.haml +0 -3
  96. data/test/templates/_partial.haml +0 -8
  97. data/test/templates/_text_area.haml +0 -3
  98. data/test/templates/_text_area_helper.html.haml +0 -4
  99. data/test/templates/action_view.haml +0 -47
  100. data/test/templates/action_view_ugly.haml +0 -47
  101. data/test/templates/breakage.haml +0 -8
  102. data/test/templates/content_for_layout.haml +0 -8
  103. data/test/templates/eval_suppressed.haml +0 -11
  104. data/test/templates/helpers.haml +0 -55
  105. data/test/templates/helpful.haml +0 -11
  106. data/test/templates/just_stuff.haml +0 -85
  107. data/test/templates/list.haml +0 -12
  108. data/test/templates/nuke_inner_whitespace.haml +0 -32
  109. data/test/templates/nuke_outer_whitespace.haml +0 -144
  110. data/test/templates/original_engine.haml +0 -17
  111. data/test/templates/partial_layout.haml +0 -3
  112. data/test/templates/partial_layout_erb.erb +0 -4
  113. data/test/templates/partialize.haml +0 -1
  114. data/test/templates/partials.haml +0 -12
  115. data/test/templates/render_layout.haml +0 -2
  116. data/test/templates/silent_script.haml +0 -45
  117. data/test/templates/standard.haml +0 -43
  118. data/test/templates/standard_ugly.haml +0 -43
  119. data/test/templates/tag_parsing.haml +0 -21
  120. data/test/templates/very_basic.haml +0 -4
  121. data/test/templates/whitespace_handling.haml +0 -87
  122. data/test/test_helper.rb +0 -81
  123. data/test/util_test.rb +0 -63
@@ -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,223 @@
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
+ end
20
+
21
+ # Returns a script to render attributes on runtime.
22
+ #
23
+ # @param attributes [Hash]
24
+ # @param object_ref [String,:nil]
25
+ # @param dynamic_attributes [DynamicAttributes]
26
+ # @return [String] Attributes rendering code
27
+ def self.runtime_build(attributes, object_ref, dynamic_attributes)
28
+ "_hamlout.attributes(#{Haml::Util.inspect_obj(attributes)}, #{object_ref},#{dynamic_attributes.to_literal})"
29
+ end
30
+
31
+ # @param options [Haml::Options]
32
+ def initialize(options)
33
+ @is_html = [:html4, :html5].include?(options[:format])
34
+ @attr_wrapper = options[:attr_wrapper]
35
+ @escape_attrs = options[:escape_attrs]
36
+ @hyphenate_data_attrs = options[:hyphenate_data_attrs]
37
+ end
38
+
39
+ # Returns Temple expression to render attributes.
40
+ #
41
+ # @param attributes [Hash]
42
+ # @param object_ref [String,:nil]
43
+ # @param dynamic_attributes [DynamicAttributes]
44
+ # @return [Array] Temple expression
45
+ def compile(attributes, object_ref, dynamic_attributes)
46
+ if object_ref != :nil || !AttributeParser.available?
47
+ return [:dynamic, AttributeCompiler.runtime_build(attributes, object_ref, dynamic_attributes)]
48
+ end
49
+
50
+ parsed_hashes = [dynamic_attributes.new, dynamic_attributes.old].compact.map do |attribute_hash|
51
+ unless (hash = AttributeParser.parse(attribute_hash))
52
+ return [:dynamic, AttributeCompiler.runtime_build(attributes, object_ref, dynamic_attributes)]
53
+ end
54
+ hash
55
+ end
56
+ attribute_values = build_attribute_values(attributes, parsed_hashes)
57
+ AttributeBuilder.verify_attribute_names!(attribute_values.map(&:key))
58
+
59
+ [:multi, *group_values_for_sort(attribute_values).map { |value_group|
60
+ compile_attribute_values(value_group)
61
+ }]
62
+ end
63
+
64
+ private
65
+
66
+ # Build array of grouped values whose sort order may go back and forth, which is also sorted with key name.
67
+ # This method needs to group values with the same start because it can be changed in `Haml::AttributeBuidler#build_data_keys`.
68
+ # @param values [Array<Haml::AttributeCompiler::AttributeValue>]
69
+ # @return [Array<Array<Haml::AttributeCompiler::AttributeValue>>]
70
+ def group_values_for_sort(values)
71
+ sorted_values = values.sort_by(&:key)
72
+ [].tap do |value_groups|
73
+ until sorted_values.empty?
74
+ key = sorted_values.first.key
75
+ value_group, sorted_values = sorted_values.partition { |v| v.key.start_with?(key) }
76
+ value_groups << value_group
77
+ end
78
+ end
79
+ end
80
+
81
+ # Returns array of AttributeValue instances from static attributes and dynamic_attributes. For each key,
82
+ # the values' order in returned value is preserved in the same order as Haml::Buffer#attributes's merge order.
83
+ #
84
+ # @param attributes [{ String => String }]
85
+ # @param parsed_hashes [{ String => String }]
86
+ # @return [Array<AttributeValue>]
87
+ def build_attribute_values(attributes, parsed_hashes)
88
+ [].tap do |attribute_values|
89
+ attributes.each do |key, static_value|
90
+ attribute_values << AttributeValue.new(:static, key, static_value)
91
+ end
92
+ parsed_hashes.each do |parsed_hash|
93
+ parsed_hash.each do |key, dynamic_value|
94
+ attribute_values << AttributeValue.new(:dynamic, key, dynamic_value)
95
+ end
96
+ end
97
+ end
98
+ end
99
+
100
+ # Compiles attribute values with the similar key to Temple expression.
101
+ #
102
+ # @param values [Array<AttributeValue>] whose `key`s are partially or fully the same from left.
103
+ # @return [Array] Temple expression
104
+ def compile_attribute_values(values)
105
+ if values.map(&:key).uniq.size == 1
106
+ compile_attribute(values.first.key, values)
107
+ else
108
+ runtime_build(values)
109
+ end
110
+ end
111
+
112
+ # @param values [Array<AttributeValue>]
113
+ # @return [Array] Temple expression
114
+ def runtime_build(values)
115
+ hash_content = values.group_by(&:key).map do |key, values_for_key|
116
+ "#{frozen_string(key)} => #{merged_value(key, values_for_key)}"
117
+ end.join(', ')
118
+ [:dynamic, "_hamlout.attributes({ #{hash_content} }, nil)"]
119
+ end
120
+
121
+ # Renders attribute values statically.
122
+ #
123
+ # @param values [Array<AttributeValue>]
124
+ # @return [Array] Temple expression
125
+ def static_build(values)
126
+ hash_content = values.group_by(&:key).map do |key, values_for_key|
127
+ "#{frozen_string(key)} => #{merged_value(key, values_for_key)}"
128
+ end.join(', ')
129
+
130
+ arguments = [@is_html, @attr_wrapper, @escape_attrs, @hyphenate_data_attrs]
131
+ code = "::Haml::AttributeBuilder.build_attributes"\
132
+ "(#{arguments.map { |a| Haml::Util.inspect_obj(a) }.join(', ')}, { #{hash_content} })"
133
+ [:static, eval(code).to_s]
134
+ end
135
+
136
+ # @param key [String]
137
+ # @param values [Array<AttributeValue>]
138
+ # @return [String]
139
+ def merged_value(key, values)
140
+ if values.size == 1
141
+ values.first.to_literal
142
+ else
143
+ "::Haml::AttributeBuilder.merge_values(#{frozen_string(key)}, #{values.map(&:to_literal).join(', ')})"
144
+ end
145
+ end
146
+
147
+ # @param str [String]
148
+ # @return [String]
149
+ def frozen_string(str)
150
+ "#{Haml::Util.inspect_obj(str)}.freeze"
151
+ end
152
+
153
+ # Compiles attribute values for one key to Temple expression that generates ` key='value'`.
154
+ #
155
+ # @param key [String]
156
+ # @param values [Array<AttributeValue>]
157
+ # @return [Array] Temple expression
158
+ def compile_attribute(key, values)
159
+ if values.all? { |v| Temple::StaticAnalyzer.static?(v.to_literal) }
160
+ return static_build(values)
161
+ end
162
+
163
+ case key
164
+ when 'id', 'class'
165
+ compile_id_or_class_attribute(key, values)
166
+ else
167
+ compile_common_attribute(key, values)
168
+ end
169
+ end
170
+
171
+ # @param id_or_class [String] "id" or "class"
172
+ # @param values [Array<AttributeValue>]
173
+ # @return [Array] Temple expression
174
+ def compile_id_or_class_attribute(id_or_class, values)
175
+ var = unique_name
176
+ [:multi,
177
+ [:code, "#{var} = (#{merged_value(id_or_class, values)})"],
178
+ [:case, var,
179
+ ['Hash, Array', runtime_build([AttributeValue.new(:dynamic, id_or_class, var)])],
180
+ ['false, nil', [:multi]],
181
+ [:else, [:multi,
182
+ [:static, " #{id_or_class}=#{@attr_wrapper}"],
183
+ [:escape, @escape_attrs, [:dynamic, var]],
184
+ [:static, @attr_wrapper]],
185
+ ]
186
+ ],
187
+ ]
188
+ end
189
+
190
+ # @param key [String] Not "id" or "class"
191
+ # @param values [Array<AttributeValue>]
192
+ # @return [Array] Temple expression
193
+ def compile_common_attribute(key, values)
194
+ var = unique_name
195
+ [:multi,
196
+ [:code, "#{var} = (#{merged_value(key, values)})"],
197
+ [:case, var,
198
+ ['Hash', runtime_build([AttributeValue.new(:dynamic, key, var)])],
199
+ ['true', true_value(key)],
200
+ ['false, nil', [:multi]],
201
+ [:else, [:multi,
202
+ [:static, " #{key}=#{@attr_wrapper}"],
203
+ [:escape, @escape_attrs, [:dynamic, var]],
204
+ [:static, @attr_wrapper]],
205
+ ]
206
+ ],
207
+ ]
208
+ end
209
+
210
+ def true_value(key)
211
+ if @is_html
212
+ [:static, " #{key}"]
213
+ else
214
+ [:static, " #{key}=#{@attr_wrapper}#{key}#{@attr_wrapper}"]
215
+ end
216
+ end
217
+
218
+ def unique_name
219
+ @unique_name ||= 0
220
+ "_haml_attribute_compiler#{@unique_name += 1}"
221
+ end
222
+ end
223
+ end
@@ -0,0 +1,148 @@
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_balanced_tokens(all_tokens) do |tokens|
102
+ key = shift_key!(tokens)
103
+ value = tokens.map {|t| t[2] }.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_balanced_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 :on_embexpr_beg
135
+ open_tokens[:embexpr] += 1
136
+ when :on_embexpr_end
137
+ open_tokens[:embexpr] -= 1
138
+ when *IGNORED_TYPES
139
+ next if attr_tokens.empty?
140
+ end
141
+
142
+ attr_tokens << token
143
+ end
144
+ block.call(attr_tokens) unless attr_tokens.empty?
145
+ end
146
+ end
147
+ end
148
+ end