haml 4.0.7 → 5.2.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (124) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/test.yml +36 -0
  3. data/.gitignore +19 -0
  4. data/.gitmodules +3 -0
  5. data/.yardopts +2 -3
  6. data/CHANGELOG.md +146 -4
  7. data/FAQ.md +4 -14
  8. data/Gemfile +16 -0
  9. data/MIT-LICENSE +2 -2
  10. data/README.md +90 -47
  11. data/REFERENCE.md +160 -74
  12. data/Rakefile +44 -63
  13. data/TODO +24 -0
  14. data/benchmark.rb +70 -0
  15. data/haml.gemspec +45 -0
  16. data/lib/haml/.gitattributes +1 -0
  17. data/lib/haml/attribute_builder.rb +219 -0
  18. data/lib/haml/attribute_compiler.rb +237 -0
  19. data/lib/haml/attribute_parser.rb +150 -0
  20. data/lib/haml/buffer.rb +12 -175
  21. data/lib/haml/compiler.rb +110 -320
  22. data/lib/haml/engine.rb +34 -41
  23. data/lib/haml/error.rb +28 -24
  24. data/lib/haml/escapable.rb +77 -0
  25. data/lib/haml/exec.rb +38 -20
  26. data/lib/haml/filters.rb +22 -27
  27. data/lib/haml/generator.rb +42 -0
  28. data/lib/haml/helpers/action_view_extensions.rb +4 -2
  29. data/lib/haml/helpers/action_view_mods.rb +45 -60
  30. data/lib/haml/helpers/action_view_xss_mods.rb +2 -0
  31. data/lib/haml/helpers/safe_erubi_template.rb +20 -0
  32. data/lib/haml/helpers/safe_erubis_template.rb +5 -1
  33. data/lib/haml/helpers/xss_mods.rb +23 -13
  34. data/lib/haml/helpers.rb +134 -89
  35. data/lib/haml/options.rb +63 -69
  36. data/lib/haml/parser.rb +319 -227
  37. data/lib/haml/plugin.rb +54 -0
  38. data/lib/haml/railtie.rb +43 -12
  39. data/lib/haml/sass_rails_filter.rb +18 -4
  40. data/lib/haml/template/options.rb +13 -2
  41. data/lib/haml/template.rb +13 -6
  42. data/lib/haml/temple_engine.rb +124 -0
  43. data/lib/haml/temple_line_counter.rb +30 -0
  44. data/lib/haml/util.rb +83 -202
  45. data/lib/haml/version.rb +3 -1
  46. data/lib/haml.rb +2 -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 +73 -115
  51. data/lib/haml/template/plugin.rb +0 -41
  52. data/test/engine_test.rb +0 -2013
  53. data/test/erb/_av_partial_1.erb +0 -12
  54. data/test/erb/_av_partial_2.erb +0 -8
  55. data/test/erb/action_view.erb +0 -62
  56. data/test/erb/standard.erb +0 -55
  57. data/test/filters_test.rb +0 -254
  58. data/test/gemfiles/Gemfile.rails-3.0.x +0 -5
  59. data/test/gemfiles/Gemfile.rails-3.1.x +0 -6
  60. data/test/gemfiles/Gemfile.rails-3.2.x +0 -5
  61. data/test/gemfiles/Gemfile.rails-4.0.x +0 -5
  62. data/test/haml-spec/LICENSE +0 -14
  63. data/test/haml-spec/README.md +0 -106
  64. data/test/haml-spec/lua_haml_spec.lua +0 -38
  65. data/test/haml-spec/perl_haml_test.pl +0 -81
  66. data/test/haml-spec/ruby_haml_test.rb +0 -23
  67. data/test/haml-spec/tests.json +0 -660
  68. data/test/helper_test.rb +0 -583
  69. data/test/markaby/standard.mab +0 -52
  70. data/test/mocks/article.rb +0 -6
  71. data/test/parser_test.rb +0 -105
  72. data/test/results/content_for_layout.xhtml +0 -12
  73. data/test/results/eval_suppressed.xhtml +0 -9
  74. data/test/results/helpers.xhtml +0 -70
  75. data/test/results/helpful.xhtml +0 -10
  76. data/test/results/just_stuff.xhtml +0 -70
  77. data/test/results/list.xhtml +0 -12
  78. data/test/results/nuke_inner_whitespace.xhtml +0 -40
  79. data/test/results/nuke_outer_whitespace.xhtml +0 -148
  80. data/test/results/original_engine.xhtml +0 -20
  81. data/test/results/partial_layout.xhtml +0 -5
  82. data/test/results/partial_layout_erb.xhtml +0 -5
  83. data/test/results/partials.xhtml +0 -21
  84. data/test/results/render_layout.xhtml +0 -3
  85. data/test/results/silent_script.xhtml +0 -74
  86. data/test/results/standard.xhtml +0 -162
  87. data/test/results/tag_parsing.xhtml +0 -23
  88. data/test/results/very_basic.xhtml +0 -5
  89. data/test/results/whitespace_handling.xhtml +0 -90
  90. data/test/template_test.rb +0 -354
  91. data/test/templates/_av_partial_1.haml +0 -9
  92. data/test/templates/_av_partial_1_ugly.haml +0 -9
  93. data/test/templates/_av_partial_2.haml +0 -5
  94. data/test/templates/_av_partial_2_ugly.haml +0 -5
  95. data/test/templates/_layout.erb +0 -3
  96. data/test/templates/_layout_for_partial.haml +0 -3
  97. data/test/templates/_partial.haml +0 -8
  98. data/test/templates/_text_area.haml +0 -3
  99. data/test/templates/_text_area_helper.html.haml +0 -4
  100. data/test/templates/action_view.haml +0 -47
  101. data/test/templates/action_view_ugly.haml +0 -47
  102. data/test/templates/breakage.haml +0 -8
  103. data/test/templates/content_for_layout.haml +0 -8
  104. data/test/templates/eval_suppressed.haml +0 -11
  105. data/test/templates/helpers.haml +0 -55
  106. data/test/templates/helpful.haml +0 -11
  107. data/test/templates/just_stuff.haml +0 -85
  108. data/test/templates/list.haml +0 -12
  109. data/test/templates/nuke_inner_whitespace.haml +0 -32
  110. data/test/templates/nuke_outer_whitespace.haml +0 -144
  111. data/test/templates/original_engine.haml +0 -17
  112. data/test/templates/partial_layout.haml +0 -3
  113. data/test/templates/partial_layout_erb.erb +0 -4
  114. data/test/templates/partialize.haml +0 -1
  115. data/test/templates/partials.haml +0 -12
  116. data/test/templates/render_layout.haml +0 -2
  117. data/test/templates/silent_script.haml +0 -45
  118. data/test/templates/standard.haml +0 -43
  119. data/test/templates/standard_ugly.haml +0 -43
  120. data/test/templates/tag_parsing.haml +0 -21
  121. data/test/templates/very_basic.haml +0 -4
  122. data/test/templates/whitespace_handling.haml +0 -87
  123. data/test/test_helper.rb +0 -81
  124. data/test/util_test.rb +0 -63
data/lib/haml/parser.rb CHANGED
@@ -1,3 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ripper'
1
4
  require 'strscan'
2
5
 
3
6
  module Haml
@@ -59,7 +62,7 @@ module Haml
59
62
  SILENT_SCRIPT,
60
63
  ESCAPE,
61
64
  FILTER
62
- ]
65
+ ].freeze
63
66
 
64
67
  # The value of the character that designates that a line is part
65
68
  # of a multiline string.
@@ -71,26 +74,28 @@ module Haml
71
74
  # foo.each do | bar |
72
75
  # = bar
73
76
  #
74
- BLOCK_WITH_SPACES = /do[\s]*\|[\s]*[^\|]*[\s]+\|\z/
77
+ BLOCK_WITH_SPACES = /do\s*\|\s*[^\|]*\s+\|\z/
75
78
 
76
- MID_BLOCK_KEYWORDS = %w[else elsif rescue ensure end when]
77
- START_BLOCK_KEYWORDS = %w[if begin case unless]
79
+ MID_BLOCK_KEYWORDS = %w[else elsif rescue ensure end when].freeze
80
+ START_BLOCK_KEYWORDS = %w[if begin case unless].freeze
78
81
  # Try to parse assignments to block starters as best as possible
79
82
  START_BLOCK_KEYWORD_REGEX = /(?:\w+(?:,\s*\w+)*\s*=\s*)?(#{START_BLOCK_KEYWORDS.join('|')})/
80
83
  BLOCK_KEYWORD_REGEX = /^-?\s*(?:(#{MID_BLOCK_KEYWORDS.join('|')})|#{START_BLOCK_KEYWORD_REGEX.source})\b/
81
84
 
82
85
  # The Regex that matches a Doctype command.
83
- DOCTYPE_REGEX = /(\d(?:\.\d)?)?[\s]*([a-z]*)\s*([^ ]+)?/i
86
+ DOCTYPE_REGEX = /(\d(?:\.\d)?)?\s*([a-z]*)\s*([^ ]+)?/i
84
87
 
85
88
  # The Regex that matches a literal string or symbol value
86
89
  LITERAL_VALUE_REGEX = /:(\w*)|(["'])((?!\\|\#\{|\#@|\#\$|\2).|\\.)*\2/
87
90
 
88
- def initialize(template, options)
89
- # :eod is a special end-of-document marker
90
- @template = (template.rstrip).split(/\r\n|\r|\n/) + [:eod, :eod]
91
- @options = options
92
- @flat = false
93
- @index = 0
91
+ ID_KEY = 'id'.freeze
92
+ CLASS_KEY = 'class'.freeze
93
+
94
+ # Used for scanning old attributes, substituting the first '{'
95
+ METHOD_CALL_PREFIX = 'a('
96
+
97
+ def initialize(options)
98
+ @options = Options.wrap(options)
94
99
  # Record the indent levels of "if" statements to validate the subsequent
95
100
  # elsif and else statements are indented at the appropriate level.
96
101
  @script_level_stack = []
@@ -98,15 +103,27 @@ module Haml
98
103
  @template_tabs = 0
99
104
  end
100
105
 
101
- def parse
106
+ def call(template)
107
+ match = template.rstrip.scan(/(([ \t]+)?(.*?))(?:\Z|\r\n|\r|\n)/m)
108
+ # discard the last match which is always blank
109
+ match.pop
110
+ @template = match.each_with_index.map do |(full, whitespace, text), index|
111
+ Line.new(whitespace, text.rstrip, full, index, self, false)
112
+ end
113
+ # Append special end-of-document marker
114
+ @template << Line.new(nil, '-#', '-#', @template.size, self, true)
115
+
102
116
  @root = @parent = ParseNode.new(:root)
103
- @haml_comment = false
117
+ @flat = false
118
+ @filter_buffer = nil
104
119
  @indentation = nil
105
120
  @line = next_line
106
121
 
107
122
  raise SyntaxError.new(Error.message(:indenting_at_start), @line.index) if @line.tabs != 0
108
123
 
109
- while next_line
124
+ loop do
125
+ next_line
126
+
110
127
  process_indent(@line) unless @line.text.empty?
111
128
 
112
129
  if flat?
@@ -118,75 +135,103 @@ module Haml
118
135
  end
119
136
 
120
137
  @tab_up = nil
121
- process_line(@line.text, @line.index) unless @line.text.empty? || @haml_comment
122
- if @parent.type != :haml_comment && (block_opened? || @tab_up)
138
+ process_line(@line) unless @line.text.empty?
139
+ if block_opened? || @tab_up
123
140
  @template_tabs += 1
124
141
  @parent = @parent.children.last
125
142
  end
126
143
 
127
- if !@haml_comment && !flat? && @next_line.tabs - @line.tabs > 1
144
+ if !flat? && @next_line.tabs - @line.tabs > 1
128
145
  raise SyntaxError.new(Error.message(:deeper_indenting, @next_line.tabs - @line.tabs), @next_line.index)
129
146
  end
130
147
 
131
148
  @line = @next_line
132
149
  end
133
-
134
150
  # Close all the open tags
135
151
  close until @parent.type == :root
136
152
  @root
137
153
  rescue Haml::Error => e
138
- e.backtrace.unshift "#{@options[:filename]}:#{(e.line ? e.line + 1 : @index) + @options[:line] - 1}"
154
+ e.backtrace.unshift "#{@options.filename}:#{(e.line ? e.line + 1 : @line.index + 1) + @options.line - 1}"
139
155
  raise
140
156
  end
141
157
 
158
+ def compute_tabs(line)
159
+ return 0 if line.text.empty? || !line.whitespace
160
+
161
+ if @indentation.nil?
162
+ @indentation = line.whitespace
163
+
164
+ if @indentation.include?(?\s) && @indentation.include?(?\t)
165
+ raise SyntaxError.new(Error.message(:cant_use_tabs_and_spaces), line.index)
166
+ end
167
+
168
+ @flat_spaces = @indentation * (@template_tabs+1) if flat?
169
+ return 1
170
+ end
171
+
172
+ tabs = line.whitespace.length / @indentation.length
173
+ return tabs if line.whitespace == @indentation * tabs
174
+ return @template_tabs + 1 if flat? && line.whitespace =~ /^#{@flat_spaces}/
175
+
176
+ message = Error.message(:inconsistent_indentation,
177
+ human_indentation(line.whitespace),
178
+ human_indentation(@indentation)
179
+ )
180
+ raise SyntaxError.new(message, line.index)
181
+ end
142
182
 
143
183
  private
144
184
 
145
185
  # @private
146
- class Line < Struct.new(:text, :unstripped, :full, :index, :compiler, :eod)
186
+ Line = Struct.new(:whitespace, :text, :full, :index, :parser, :eod) do
147
187
  alias_method :eod?, :eod
148
188
 
149
189
  # @private
150
190
  def tabs
151
- line = self
152
- @tabs ||= compiler.instance_eval do
153
- break 0 if line.text.empty? || !(whitespace = line.full[/^\s+/])
154
-
155
- if @indentation.nil?
156
- @indentation = whitespace
157
-
158
- if @indentation.include?(?\s) && @indentation.include?(?\t)
159
- raise SyntaxError.new(Error.message(:cant_use_tabs_and_spaces), line.index)
160
- end
161
-
162
- @flat_spaces = @indentation * (@template_tabs+1) if flat?
163
- break 1
164
- end
165
-
166
- tabs = whitespace.length / @indentation.length
167
- break tabs if whitespace == @indentation * tabs
168
- break @template_tabs + 1 if flat? && whitespace =~ /^#{@flat_spaces}/
191
+ @tabs ||= parser.compute_tabs(self)
192
+ end
169
193
 
170
- message = Error.message(:inconsistent_indentation,
171
- Haml::Util.human_indentation(whitespace),
172
- Haml::Util.human_indentation(@indentation)
173
- )
174
- raise SyntaxError.new(message, line.index)
175
- end
194
+ def strip!(from)
195
+ self.text = text[from..-1]
196
+ self.text.lstrip!
197
+ self
176
198
  end
177
199
  end
178
200
 
179
201
  # @private
180
- class ParseNode < Struct.new(:type, :line, :value, :parent, :children)
202
+ ParseNode = Struct.new(:type, :line, :value, :parent, :children) do
181
203
  def initialize(*args)
182
204
  super
183
205
  self.children ||= []
184
206
  end
185
207
 
186
208
  def inspect
187
- text = "(#{type} #{value.inspect}"
188
- children.each {|c| text << "\n" << c.inspect.gsub(/^/, " ")}
189
- text + ")"
209
+ %Q[(#{type} #{value.inspect}#{children.each_with_object(''.dup) {|c, s| s << "\n#{c.inspect.gsub!(/^/, ' ')}"}})].dup
210
+ end
211
+ end
212
+
213
+ # @param [String] new - Hash literal including dynamic values.
214
+ # @param [String] old - Hash literal including dynamic values or Ruby literal of multiple Hashes which MUST be interpreted as method's last arguments.
215
+ DynamicAttributes = Struct.new(:new, :old) do
216
+ undef :old=
217
+ def old=(value)
218
+ unless value =~ /\A{.*}\z/m
219
+ raise ArgumentError.new('Old attributes must start with "{" and end with "}"')
220
+ end
221
+ self[:old] = value
222
+ end
223
+
224
+ # This will be a literal for Haml::Buffer#attributes's last argument, `attributes_hashes`.
225
+ def to_literal
226
+ [new, stripped_old].compact.join(', ')
227
+ end
228
+
229
+ private
230
+
231
+ # For `%foo{ { foo: 1 }, bar: 2 }`, :old is "{ { foo: 1 }, bar: 2 }" and this method returns " { foo: 1 }, bar: 2 " for last argument.
232
+ def stripped_old
233
+ return nil if old.nil?
234
+ old.sub!(/\A{/, '').sub!(/}\z/m, '')
190
235
  end
191
236
  end
192
237
 
@@ -195,98 +240,104 @@ module Haml
195
240
  return unless line.tabs <= @template_tabs && @template_tabs > 0
196
241
 
197
242
  to_close = @template_tabs - line.tabs
198
- to_close.times {|i| close unless to_close - 1 - i == 0 && mid_block_keyword?(line.text)}
243
+ to_close.times {|i| close unless to_close - 1 - i == 0 && continuation_script?(line.text)}
244
+ end
245
+
246
+ def continuation_script?(text)
247
+ text[0] == SILENT_SCRIPT && mid_block_keyword?(text)
248
+ end
249
+
250
+ def mid_block_keyword?(text)
251
+ MID_BLOCK_KEYWORDS.include?(block_keyword(text))
199
252
  end
200
253
 
201
254
  # Processes a single line of Haml.
202
255
  #
203
256
  # This method doesn't return anything; it simply processes the line and
204
257
  # adds the appropriate code to `@precompiled`.
205
- def process_line(text, index)
206
- @index = index + 1
207
-
208
- case text[0]
209
- when DIV_CLASS; push div(text)
258
+ def process_line(line)
259
+ case line.text[0]
260
+ when DIV_CLASS; push div(line)
210
261
  when DIV_ID
211
- return push plain(text) if text[1] == ?{
212
- push div(text)
213
- when ELEMENT; push tag(text)
214
- when COMMENT; push comment(text[1..-1].strip)
262
+ return push plain(line) if %w[{ @ $].include?(line.text[1])
263
+ push div(line)
264
+ when ELEMENT; push tag(line)
265
+ when COMMENT; push comment(line.text[1..-1].lstrip)
215
266
  when SANITIZE
216
- return push plain(text[3..-1].strip, :escape_html) if text[1..2] == "=="
217
- return push script(text[2..-1].strip, :escape_html) if text[1] == SCRIPT
218
- return push flat_script(text[2..-1].strip, :escape_html) if text[1] == FLAT_SCRIPT
219
- return push plain(text[1..-1].strip, :escape_html) if text[1] == ?\s
220
- push plain(text)
267
+ return push plain(line.strip!(3), :escape_html) if line.text[1, 2] == '=='
268
+ return push script(line.strip!(2), :escape_html) if line.text[1] == SCRIPT
269
+ return push flat_script(line.strip!(2), :escape_html) if line.text[1] == FLAT_SCRIPT
270
+ return push plain(line.strip!(1), :escape_html) if line.text[1] == ?\s || line.text[1..2] == '#{'
271
+ push plain(line)
221
272
  when SCRIPT
222
- return push plain(text[2..-1].strip) if text[1] == SCRIPT
223
- push script(text[1..-1])
224
- when FLAT_SCRIPT; push flat_script(text[1..-1])
225
- when SILENT_SCRIPT; push silent_script(text)
226
- when FILTER; push filter(text[1..-1].downcase)
273
+ return push plain(line.strip!(2)) if line.text[1] == SCRIPT
274
+ line.text = line.text[1..-1]
275
+ push script(line)
276
+ when FLAT_SCRIPT; push flat_script(line.strip!(1))
277
+ when SILENT_SCRIPT
278
+ return push haml_comment(line.text[2..-1]) if line.text[1] == SILENT_COMMENT
279
+ push silent_script(line)
280
+ when FILTER; push filter(line.text[1..-1].downcase)
227
281
  when DOCTYPE
228
- return push doctype(text) if text[0...3] == '!!!'
229
- return push plain(text[3..-1].strip, false) if text[1..2] == "=="
230
- return push script(text[2..-1].strip, false) if text[1] == SCRIPT
231
- return push flat_script(text[2..-1].strip, false) if text[1] == FLAT_SCRIPT
232
- return push plain(text[1..-1].strip, false) if text[1] == ?\s
233
- push plain(text)
234
- when ESCAPE; push plain(text[1..-1])
235
- else; push plain(text)
282
+ return push doctype(line.text) if line.text[0, 3] == '!!!'
283
+ return push plain(line.strip!(3), false) if line.text[1, 2] == '=='
284
+ return push script(line.strip!(2), false) if line.text[1] == SCRIPT
285
+ return push flat_script(line.strip!(2), false) if line.text[1] == FLAT_SCRIPT
286
+ return push plain(line.strip!(1), false) if line.text[1] == ?\s || line.text[1..2] == '#{'
287
+ push plain(line)
288
+ when ESCAPE
289
+ line.text = line.text[1..-1]
290
+ push plain(line)
291
+ else; push plain(line)
236
292
  end
237
293
  end
238
294
 
239
295
  def block_keyword(text)
240
- return unless keyword = text.scan(BLOCK_KEYWORD_REGEX)[0]
296
+ return unless (keyword = text.scan(BLOCK_KEYWORD_REGEX)[0])
241
297
  keyword[0] || keyword[1]
242
298
  end
243
299
 
244
- def mid_block_keyword?(text)
245
- MID_BLOCK_KEYWORDS.include?(block_keyword(text))
246
- end
247
-
248
300
  def push(node)
249
301
  @parent.children << node
250
302
  node.parent = @parent
251
303
  end
252
304
 
253
- def plain(text, escape_html = nil)
305
+ def plain(line, escape_html = nil)
254
306
  if block_opened?
255
307
  raise SyntaxError.new(Error.message(:illegal_nesting_plain), @next_line.index)
256
308
  end
257
309
 
258
- unless contains_interpolation?(text)
259
- return ParseNode.new(:plain, @index, :text => text)
310
+ unless contains_interpolation?(line.text)
311
+ return ParseNode.new(:plain, line.index + 1, :text => line.text)
260
312
  end
261
313
 
262
- escape_html = @options[:escape_html] if escape_html.nil?
263
- script(unescape_interpolation(text, escape_html), false)
314
+ escape_html = @options.escape_html && @options.mime_type != 'text/plain' if escape_html.nil?
315
+ line.text = unescape_interpolation(line.text, escape_html)
316
+ script(line, false)
264
317
  end
265
318
 
266
- def script(text, escape_html = nil, preserve = false)
267
- raise SyntaxError.new(Error.message(:no_ruby_code, '=')) if text.empty?
268
- text = handle_ruby_multiline(text)
269
- escape_html = @options[:escape_html] if escape_html.nil?
319
+ def script(line, escape_html = nil, preserve = false)
320
+ raise SyntaxError.new(Error.message(:no_ruby_code, '=')) if line.text.empty?
321
+ line = handle_ruby_multiline(line)
322
+ escape_html = @options.escape_html if escape_html.nil?
270
323
 
271
- keyword = block_keyword(text)
324
+ keyword = block_keyword(line.text)
272
325
  check_push_script_stack(keyword)
273
326
 
274
- ParseNode.new(:script, @index, :text => text, :escape_html => escape_html,
327
+ ParseNode.new(:script, line.index + 1, :text => line.text, :escape_html => escape_html,
275
328
  :preserve => preserve, :keyword => keyword)
276
329
  end
277
330
 
278
- def flat_script(text, escape_html = nil)
279
- raise SyntaxError.new(Error.message(:no_ruby_code, '~')) if text.empty?
280
- script(text, escape_html, :preserve)
331
+ def flat_script(line, escape_html = nil)
332
+ raise SyntaxError.new(Error.message(:no_ruby_code, '~')) if line.text.empty?
333
+ script(line, escape_html, :preserve)
281
334
  end
282
335
 
283
- def silent_script(text)
284
- return haml_comment(text[2..-1]) if text[1] == SILENT_COMMENT
285
-
286
- raise SyntaxError.new(Error.message(:no_end), @index - 1) if text[1..-1].strip == "end"
336
+ def silent_script(line)
337
+ raise SyntaxError.new(Error.message(:no_end), line.index) if line.text[1..-1].strip == 'end'
287
338
 
288
- text = handle_ruby_multiline(text)
289
- keyword = block_keyword(text)
339
+ line = handle_ruby_multiline(line)
340
+ keyword = block_keyword(line.text)
290
341
 
291
342
  check_push_script_stack(keyword)
292
343
 
@@ -308,8 +359,8 @@ module Haml
308
359
  end
309
360
  end
310
361
 
311
- ParseNode.new(:silent_script, @index,
312
- :text => text[1..-1], :keyword => keyword)
362
+ ParseNode.new(:silent_script, @line.index + 1,
363
+ :text => line.text[1..-1], :keyword => keyword)
313
364
  end
314
365
 
315
366
  def check_push_script_stack(keyword)
@@ -323,18 +374,25 @@ module Haml
323
374
  end
324
375
 
325
376
  def haml_comment(text)
326
- @haml_comment = block_opened?
327
- ParseNode.new(:haml_comment, @index, :text => text)
377
+ if filter_opened?
378
+ @flat = true
379
+ @filter_buffer = String.new
380
+ @filter_buffer << "#{text}\n" unless text.empty?
381
+ text = @filter_buffer
382
+ # If we don't know the indentation by now, it'll be set in Line#tabs
383
+ @flat_spaces = @indentation * (@template_tabs+1) if @indentation
384
+ end
385
+
386
+ ParseNode.new(:haml_comment, @line.index + 1, :text => text)
328
387
  end
329
388
 
330
389
  def tag(line)
331
390
  tag_name, attributes, attributes_hashes, object_ref, nuke_outer_whitespace,
332
- nuke_inner_whitespace, action, value, last_line = parse_tag(line)
391
+ nuke_inner_whitespace, action, value, last_line = parse_tag(line.text)
333
392
 
334
- preserve_tag = @options[:preserve].include?(tag_name)
393
+ preserve_tag = @options.preserve.include?(tag_name)
335
394
  nuke_inner_whitespace ||= preserve_tag
336
- preserve_tag = false if @options[:ugly]
337
- escape_html = (action == '&' || (action != '!' && @options[:escape_html]))
395
+ escape_html = (action == '&' || (action != '!' && @options.escape_html))
338
396
 
339
397
  case action
340
398
  when '/'; self_closing = true
@@ -369,22 +427,20 @@ module Haml
369
427
  end
370
428
 
371
429
  attributes = Parser.parse_class_and_id(attributes)
372
- attributes_list = []
430
+ dynamic_attributes = DynamicAttributes.new
373
431
 
374
432
  if attributes_hashes[:new]
375
433
  static_attributes, attributes_hash = attributes_hashes[:new]
376
- Buffer.merge_attrs(attributes, static_attributes) if static_attributes
377
- attributes_list << attributes_hash
434
+ AttributeBuilder.merge_attributes!(attributes, static_attributes) if static_attributes
435
+ dynamic_attributes.new = attributes_hash
378
436
  end
379
437
 
380
438
  if attributes_hashes[:old]
381
439
  static_attributes = parse_static_hash(attributes_hashes[:old])
382
- Buffer.merge_attrs(attributes, static_attributes) if static_attributes
383
- attributes_list << attributes_hashes[:old] unless static_attributes || @options[:suppress_eval]
440
+ AttributeBuilder.merge_attributes!(attributes, static_attributes) if static_attributes
441
+ dynamic_attributes.old = attributes_hashes[:old] unless static_attributes || @options.suppress_eval
384
442
  end
385
443
 
386
- attributes_list.compact!
387
-
388
444
  raise SyntaxError.new(Error.message(:illegal_nesting_self_closing), @next_line.index) if block_opened? && self_closing
389
445
  raise SyntaxError.new(Error.message(:no_ruby_code, action), last_line - 1) if parse && value.empty?
390
446
  raise SyntaxError.new(Error.message(:self_closing_content), last_line - 1) if self_closing && !value.empty?
@@ -393,56 +449,70 @@ module Haml
393
449
  raise SyntaxError.new(Error.message(:illegal_nesting_line, tag_name), @next_line.index)
394
450
  end
395
451
 
396
- self_closing ||= !!(!block_opened? && value.empty? && @options[:autoclose].any? {|t| t === tag_name})
452
+ self_closing ||= !!(!block_opened? && value.empty? && @options.autoclose.any? {|t| t === tag_name})
397
453
  value = nil if value.empty? && (block_opened? || self_closing)
398
- value = handle_ruby_multiline(value) if parse
454
+ line.text = value
455
+ line = handle_ruby_multiline(line) if parse
399
456
 
400
- ParseNode.new(:tag, @index, :name => tag_name, :attributes => attributes,
401
- :attributes_hashes => attributes_list, :self_closing => self_closing,
457
+ ParseNode.new(:tag, line.index + 1, :name => tag_name, :attributes => attributes,
458
+ :dynamic_attributes => dynamic_attributes, :self_closing => self_closing,
402
459
  :nuke_inner_whitespace => nuke_inner_whitespace,
403
460
  :nuke_outer_whitespace => nuke_outer_whitespace, :object_ref => object_ref,
404
461
  :escape_html => escape_html, :preserve_tag => preserve_tag,
405
- :preserve_script => preserve_script, :parse => parse, :value => value)
462
+ :preserve_script => preserve_script, :parse => parse, :value => line.text)
406
463
  end
407
464
 
408
465
  # Renders a line that creates an XHTML tag and has an implicit div because of
409
466
  # `.` or `#`.
410
467
  def div(line)
411
- tag('%div' + line)
468
+ line.text = "%div#{line.text}"
469
+ tag(line)
412
470
  end
413
471
 
414
472
  # Renders an XHTML comment.
415
- def comment(line)
416
- conditional, line = balance(line, ?[, ?]) if line[0] == ?[
417
- line.strip!
418
- conditional << ">" if conditional
473
+ def comment(text)
474
+ if text[0..1] == '!['
475
+ revealed = true
476
+ text = text[1..-1]
477
+ else
478
+ revealed = false
479
+ end
419
480
 
420
- if block_opened? && !line.empty?
481
+ conditional, text = balance(text, ?[, ?]) if text[0] == ?[
482
+ text.strip!
483
+
484
+ if contains_interpolation?(text)
485
+ parse = true
486
+ text = unescape_interpolation(text)
487
+ else
488
+ parse = false
489
+ end
490
+
491
+ if block_opened? && !text.empty?
421
492
  raise SyntaxError.new(Haml::Error.message(:illegal_nesting_content), @next_line.index)
422
493
  end
423
494
 
424
- ParseNode.new(:comment, @index, :conditional => conditional, :text => line)
495
+ ParseNode.new(:comment, @line.index + 1, :conditional => conditional, :text => text, :revealed => revealed, :parse => parse)
425
496
  end
426
497
 
427
498
  # Renders an XHTML doctype or XML shebang.
428
- def doctype(line)
499
+ def doctype(text)
429
500
  raise SyntaxError.new(Error.message(:illegal_nesting_header), @next_line.index) if block_opened?
430
- version, type, encoding = line[3..-1].strip.downcase.scan(DOCTYPE_REGEX)[0]
431
- ParseNode.new(:doctype, @index, :version => version, :type => type, :encoding => encoding)
501
+ version, type, encoding = text[3..-1].strip.downcase.scan(DOCTYPE_REGEX)[0]
502
+ ParseNode.new(:doctype, @line.index + 1, :version => version, :type => type, :encoding => encoding)
432
503
  end
433
504
 
434
505
  def filter(name)
435
506
  raise Error.new(Error.message(:invalid_filter_name, name)) unless name =~ /^\w+$/
436
507
 
437
- @filter_buffer = String.new
438
-
439
508
  if filter_opened?
440
509
  @flat = true
510
+ @filter_buffer = String.new
441
511
  # If we don't know the indentation by now, it'll be set in Line#tabs
442
512
  @flat_spaces = @indentation * (@template_tabs+1) if @indentation
443
513
  end
444
514
 
445
- ParseNode.new(:filter, @index, :name => name, :text => @filter_buffer)
515
+ ParseNode.new(:filter, @line.index + 1, :name => name, :text => @filter_buffer)
446
516
  end
447
517
 
448
518
  def close
@@ -452,13 +522,17 @@ module Haml
452
522
  end
453
523
 
454
524
  def close_filter(_)
455
- @flat = false
456
- @flat_spaces = nil
457
- @filter_buffer = nil
525
+ close_flat_section
458
526
  end
459
527
 
460
528
  def close_haml_comment(_)
461
- @haml_comment = false
529
+ close_flat_section
530
+ end
531
+
532
+ def close_flat_section
533
+ @flat = false
534
+ @flat_spaces = nil
535
+ @filter_buffer = nil
462
536
  end
463
537
 
464
538
  def close_silent_script(node)
@@ -466,7 +540,7 @@ module Haml
466
540
 
467
541
  # Post-process case statements to normalize the nesting of "when" clauses
468
542
  return unless node.value[:keyword] == "case"
469
- return unless first = node.children.first
543
+ return unless (first = node.children.first)
470
544
  return unless first.type == :silent_script && first.value[:keyword] == "when"
471
545
  return if first.children.empty?
472
546
  # If the case node has a "when" child with children, it's the
@@ -485,29 +559,39 @@ module Haml
485
559
  # that can then be merged with another attributes hash.
486
560
  def self.parse_class_and_id(list)
487
561
  attributes = {}
488
- list.scan(/([#.])([-:_a-zA-Z0-9]+)/) do |type, property|
562
+ return attributes if list.empty?
563
+
564
+ list.scan(/([#.])([-:_a-zA-Z0-9\@]+)/) do |type, property|
489
565
  case type
490
566
  when '.'
491
- if attributes['class']
492
- attributes['class'] += " "
567
+ if attributes[CLASS_KEY]
568
+ attributes[CLASS_KEY] += " "
493
569
  else
494
- attributes['class'] = ""
570
+ attributes[CLASS_KEY] = ""
495
571
  end
496
- attributes['class'] += property
497
- when '#'; attributes['id'] = property
572
+ attributes[CLASS_KEY] += property
573
+ when '#'; attributes[ID_KEY] = property
498
574
  end
499
575
  end
500
576
  attributes
501
577
  end
502
578
 
579
+ # This method doesn't use Haml::AttributeParser because currently it depends on Ripper and Rubinius doesn't provide it.
580
+ # Ideally this logic should be placed in Haml::AttributeParser instead of here and this method should use it.
581
+ #
582
+ # @param [String] text - Hash literal or text inside old attributes
583
+ # @return [Hash,nil] - Return nil if text is not static Hash literal
503
584
  def parse_static_hash(text)
504
585
  attributes = {}
586
+ return attributes if text.empty?
587
+
588
+ text = text[1...-1] # strip brackets
505
589
  scanner = StringScanner.new(text)
506
590
  scanner.scan(/\s+/)
507
591
  until scanner.eos?
508
- return unless key = scanner.scan(LITERAL_VALUE_REGEX)
592
+ return unless (key = scanner.scan(LITERAL_VALUE_REGEX))
509
593
  return unless scanner.scan(/\s*=>\s*/)
510
- return unless value = scanner.scan(LITERAL_VALUE_REGEX)
594
+ return unless (value = scanner.scan(LITERAL_VALUE_REGEX))
511
595
  return unless scanner.scan(/\s*(?:,|$)\s*/)
512
596
  attributes[eval(key).to_s] = eval(value).to_s
513
597
  end
@@ -515,20 +599,20 @@ module Haml
515
599
  end
516
600
 
517
601
  # Parses a line into tag_name, attributes, attributes_hash, object_ref, action, value
518
- def parse_tag(line)
519
- match = line.scan(/%([-:\w]+)([-:\w\.\#]*)(.*)/)[0]
520
- raise SyntaxError.new(Error.message(:invalid_tag, line)) unless match
602
+ def parse_tag(text)
603
+ match = text.scan(/%([-:\w]+)([-:\w.#\@]*)(.+)?/)[0]
604
+ raise SyntaxError.new(Error.message(:invalid_tag, text)) unless match
521
605
 
522
606
  tag_name, attributes, rest = match
523
607
 
524
- if attributes =~ /[\.#](\.|#|\z)/
608
+ if !attributes.empty? && (attributes =~ /[.#](\.|#|\z)/)
525
609
  raise SyntaxError.new(Error.message(:illegal_element))
526
610
  end
527
611
 
528
612
  new_attributes_hash = old_attributes_hash = last_line = nil
529
- object_ref = "nil"
613
+ object_ref = :nil
530
614
  attributes_hashes = {}
531
- while rest
615
+ while rest && !rest.empty?
532
616
  case rest[0]
533
617
  when ?{
534
618
  break if old_attributes_hash
@@ -539,38 +623,51 @@ module Haml
539
623
  new_attributes_hash, rest, last_line = parse_new_attributes(rest)
540
624
  attributes_hashes[:new] = new_attributes_hash
541
625
  when ?[
542
- break unless object_ref == "nil"
626
+ break unless object_ref == :nil
543
627
  object_ref, rest = balance(rest, ?[, ?])
544
628
  else; break
545
629
  end
546
630
  end
547
631
 
548
- if rest
632
+ if rest && !rest.empty?
549
633
  nuke_whitespace, action, value = rest.scan(/(<>|><|[><])?([=\/\~&!])?(.*)?/)[0]
550
- nuke_whitespace ||= ''
551
- nuke_outer_whitespace = nuke_whitespace.include? '>'
552
- nuke_inner_whitespace = nuke_whitespace.include? '<'
634
+ if nuke_whitespace
635
+ nuke_outer_whitespace = nuke_whitespace.include? '>'
636
+ nuke_inner_whitespace = nuke_whitespace.include? '<'
637
+ end
553
638
  end
554
639
 
555
- if @options[:remove_whitespace]
640
+ if @options.remove_whitespace
556
641
  nuke_outer_whitespace = true
557
642
  nuke_inner_whitespace = true
558
643
  end
559
644
 
560
- value = value.to_s.strip
645
+ if value.nil?
646
+ value = ''
647
+ else
648
+ value.strip!
649
+ end
561
650
  [tag_name, attributes, attributes_hashes, object_ref, nuke_outer_whitespace,
562
- nuke_inner_whitespace, action, value, last_line || @index]
651
+ nuke_inner_whitespace, action, value, last_line || @line.index + 1]
563
652
  end
564
653
 
565
- def parse_old_attributes(line)
566
- line = line.dup
567
- last_line = @index
654
+ # @return [String] attributes_hash - Hash literal starting with `{` and ending with `}`
655
+ # @return [String] rest
656
+ # @return [Integer] last_line
657
+ def parse_old_attributes(text)
658
+ last_line = @line.index + 1
568
659
 
569
660
  begin
570
- attributes_hash, rest = balance(line, ?{, ?})
661
+ # Old attributes often look like a valid Hash literal, but it sometimes allow code like
662
+ # `{ hash, foo: bar }`, which is compiled to `_hamlout.attributes({}, nil, hash, foo: bar)`.
663
+ #
664
+ # To scan such code correctly, this scans `a( hash, foo: bar }` instead, stops when there is
665
+ # 1 more :on_embexpr_end (the last '}') than :on_embexpr_beg, and resurrects '{' afterwards.
666
+ balanced, rest = balance_tokens(text.sub(?{, METHOD_CALL_PREFIX), :on_embexpr_beg, :on_embexpr_end, count: 1)
667
+ attributes_hash = balanced.sub(METHOD_CALL_PREFIX, ?{)
571
668
  rescue SyntaxError => e
572
- if line.strip[-1] == ?, && e.message == Error.message(:unbalanced_brackets)
573
- line << "\n" << @next_line.text
669
+ if e.message == Error.message(:unbalanced_brackets) && !@template.empty?
670
+ text << "\n#{@next_line.text}"
574
671
  last_line += 1
575
672
  next_line
576
673
  retry
@@ -579,14 +676,15 @@ module Haml
579
676
  raise e
580
677
  end
581
678
 
582
- attributes_hash = attributes_hash[1...-1] if attributes_hash
583
679
  return attributes_hash, rest, last_line
584
680
  end
585
681
 
586
- def parse_new_attributes(line)
587
- line = line.dup
588
- scanner = StringScanner.new(line)
589
- last_line = @index
682
+ # @return [Array<Hash,String,nil>] - [static_attributes (Hash), dynamic_attributes (nil or String starting with `{` and ending with `}`)]
683
+ # @return [String] rest
684
+ # @return [Integer] last_line
685
+ def parse_new_attributes(text)
686
+ scanner = StringScanner.new(text)
687
+ last_line = @line.index + 1
590
688
  attributes = {}
591
689
 
592
690
  scanner.scan(/\(\s*/)
@@ -595,14 +693,15 @@ module Haml
595
693
  break if name.nil?
596
694
 
597
695
  if name == false
598
- text = (Haml::Util.balance(line, ?(, ?)) || [line]).first
696
+ scanned = Haml::Util.balance(text, ?(, ?))
697
+ text = scanned ? scanned.first : text
599
698
  raise Haml::SyntaxError.new(Error.message(:invalid_attribute_list, text.inspect), last_line - 1)
600
699
  end
601
700
  attributes[name] = value
602
701
  scanner.scan(/\s*/)
603
702
 
604
703
  if scanner.eos?
605
- line << " " << @next_line.text
704
+ text << " #{@next_line.text}"
606
705
  last_line += 1
607
706
  next_line
608
707
  scanner.scan(/\s*/)
@@ -610,12 +709,12 @@ module Haml
610
709
  end
611
710
 
612
711
  static_attributes = {}
613
- dynamic_attributes = "{"
712
+ dynamic_attributes = "{".dup
614
713
  attributes.each do |name, (type, val)|
615
714
  if type == :static
616
715
  static_attributes[name] = val
617
716
  else
618
- dynamic_attributes << inspect_obj(name) << " => " << val << ","
717
+ dynamic_attributes << "#{inspect_obj(name)} => #{val},"
619
718
  end
620
719
  end
621
720
  dynamic_attributes << "}"
@@ -625,7 +724,7 @@ module Haml
625
724
  end
626
725
 
627
726
  def parse_new_attribute(scanner)
628
- unless name = scanner.scan(/[-:\w]+/)
727
+ unless (name = scanner.scan(/[-:\w]+/))
629
728
  return if scanner.scan(/\)/)
630
729
  return false
631
730
  end
@@ -634,8 +733,8 @@ module Haml
634
733
  return name, [:static, true] unless scanner.scan(/=/) #/end
635
734
 
636
735
  scanner.scan(/\s*/)
637
- unless quote = scanner.scan(/["']/)
638
- return false unless var = scanner.scan(/(@@?|\$)?\w+/)
736
+ unless (quote = scanner.scan(/["']/))
737
+ return false unless (var = scanner.scan(/(@@?|\$)?\w+/))
639
738
  return name, [:dynamic, var]
640
739
  end
641
740
 
@@ -650,35 +749,16 @@ module Haml
650
749
 
651
750
  return name, [:static, content.first[1]] if content.size == 1
652
751
  return name, [:dynamic,
653
- '"' + content.map {|(t, v)| t == :str ? inspect_obj(v)[1...-1] : "\#{#{v}}"}.join + '"']
654
- end
655
-
656
- def raw_next_line
657
- text = @template.shift
658
- return unless text
659
-
660
- index = @template_index
661
- @template_index += 1
662
-
663
- return text, index
752
+ %!"#{content.each_with_object(''.dup) {|(t, v), s| s << (t == :str ? inspect_obj(v)[1...-1] : "\#{#{v}}")}}"!]
664
753
  end
665
754
 
666
755
  def next_line
667
- text, index = raw_next_line
668
- return unless text
669
-
670
- # :eod is a special end-of-document marker
671
- line =
672
- if text == :eod
673
- Line.new '-#', '-#', '-#', index, self, true
674
- else
675
- Line.new text.strip, text.lstrip.chomp, text, index, self, false
676
- end
756
+ line = @template.shift || raise(StopIteration)
677
757
 
678
758
  # `flat?' here is a little outdated,
679
759
  # so we have to manually check if either the previous or current line
680
760
  # closes the flat block, as well as whether a new block is opened.
681
- line_defined = instance_variable_defined?('@line')
761
+ line_defined = instance_variable_defined?(:@line)
682
762
  @line.tabs if line_defined
683
763
  unless (flat? && !closes_flat?(line) && !closes_flat?(@line)) ||
684
764
  (line_defined && @line.text[0] == ?: && line.full =~ %r[^#{@line.full[/^\s+/]}\s])
@@ -694,21 +774,17 @@ module Haml
694
774
  line && !line.text.empty? && line.full !~ /^#{@flat_spaces}/
695
775
  end
696
776
 
697
- def un_next_line(line)
698
- @template.unshift line
699
- @template_index -= 1
700
- end
701
-
702
777
  def handle_multiline(line)
703
778
  return unless is_multiline?(line.text)
704
779
  line.text.slice!(-1)
705
- while new_line = raw_next_line.first
706
- break if new_line == :eod
707
- next if new_line.strip.empty?
708
- break unless is_multiline?(new_line.strip)
709
- line.text << new_line.strip[0...-1]
780
+ loop do
781
+ new_line = @template.first
782
+ break if new_line.eod?
783
+ next @template.shift if new_line.text.strip.empty?
784
+ break unless is_multiline?(new_line.text.strip)
785
+ line.text << new_line.text.strip[0...-1]
786
+ @template.shift
710
787
  end
711
- un_next_line new_line
712
788
  end
713
789
 
714
790
  # Checks whether or not `line` is in a multiline sequence.
@@ -716,18 +792,18 @@ module Haml
716
792
  text && text.length > 1 && text[-1] == MULTILINE_CHAR_VALUE && text[-2] == ?\s && text !~ BLOCK_WITH_SPACES
717
793
  end
718
794
 
719
- def handle_ruby_multiline(text)
720
- text = text.rstrip
721
- return text unless is_ruby_multiline?(text)
722
- un_next_line @next_line.full
795
+ def handle_ruby_multiline(line)
796
+ line.text.rstrip!
797
+ return line unless is_ruby_multiline?(line.text)
723
798
  begin
724
- new_line = raw_next_line.first
725
- break if new_line == :eod
726
- next if new_line.strip.empty?
727
- text << " " << new_line.strip
728
- end while is_ruby_multiline?(new_line.strip)
799
+ # Use already fetched @next_line in the first loop. Otherwise, fetch next
800
+ new_line = new_line.nil? ? @next_line : @template.shift
801
+ break if new_line.eod?
802
+ next if new_line.text.empty?
803
+ line.text << " #{new_line.text.rstrip}"
804
+ end while is_ruby_multiline?(new_line.text)
729
805
  next_line
730
- text
806
+ line
731
807
  end
732
808
 
733
809
  # `text' is a Ruby multiline block if it:
@@ -735,15 +811,31 @@ module Haml
735
811
  # - but not "?," which is a character literal
736
812
  # (however, "x?," is a method call and not a literal)
737
813
  # - and not "?\," which is a character literal
738
- #
739
814
  def is_ruby_multiline?(text)
740
815
  text && text.length > 1 && text[-1] == ?, &&
741
- !((text[-3..-2] =~ /\W\?/) || text[-3..-2] == "?\\")
816
+ !((text[-3, 2] =~ /\W\?/) || text[-3, 2] == "?\\")
742
817
  end
743
818
 
744
819
  def balance(*args)
745
- res = Haml::Util.balance(*args)
746
- return res if res
820
+ Haml::Util.balance(*args) or raise(SyntaxError.new(Error.message(:unbalanced_brackets)))
821
+ end
822
+
823
+ # Unlike #balance, this balances Ripper tokens to balance something like `{ a: "}" }` correctly.
824
+ def balance_tokens(buf, start, finish, count: 0)
825
+ text = ''.dup
826
+ Ripper.lex(buf).each do |_, token, str|
827
+ text << str
828
+ case token
829
+ when start
830
+ count += 1
831
+ when finish
832
+ count -= 1
833
+ end
834
+
835
+ if count == 0
836
+ return text, buf.sub(text, '')
837
+ end
838
+ end
747
839
  raise SyntaxError.new(Error.message(:unbalanced_brackets))
748
840
  end
749
841
 
@@ -754,7 +846,7 @@ module Haml
754
846
  # Same semantics as block_opened?, except that block_opened? uses Line#tabs,
755
847
  # which doesn't interact well with filter lines
756
848
  def filter_opened?
757
- @next_line.full =~ (@indentation ? /^#{@indentation * @template_tabs}/ : /^\s/)
849
+ @next_line.full =~ (@indentation ? /^#{@indentation * (@template_tabs + 1)}/ : /^\s/)
758
850
  end
759
851
 
760
852
  def flat?