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