haml 4.0.6 → 5.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (83) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +1 -1
  3. data/CHANGELOG.md +49 -4
  4. data/FAQ.md +4 -14
  5. data/MIT-LICENSE +1 -1
  6. data/README.md +85 -42
  7. data/REFERENCE.md +109 -58
  8. data/Rakefile +46 -54
  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 +26 -136
  13. data/lib/haml/compiler.rb +87 -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 +18 -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 +36 -58
  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 +4 -1
  25. data/lib/haml/helpers/xss_mods.rb +18 -12
  26. data/lib/haml/helpers.rb +133 -90
  27. data/lib/haml/options.rb +38 -47
  28. data/lib/haml/parser.rb +278 -216
  29. data/lib/haml/{template/plugin.rb → plugin.rb} +8 -15
  30. data/lib/haml/railtie.rb +21 -12
  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 +1 -0
  39. data/test/attribute_parser_test.rb +101 -0
  40. data/test/engine_test.rb +287 -176
  41. data/test/filters_test.rb +32 -19
  42. data/test/gemfiles/Gemfile.rails-4.0.x +9 -3
  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 +224 -112
  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 +77 -76
  67. data/test/template_test.rb +24 -56
  68. data/test/template_test_helper.rb +38 -0
  69. data/test/templates/bemit.haml +3 -0
  70. data/test/templates/just_stuff.haml +1 -0
  71. data/test/templates/standard_ugly.haml +1 -0
  72. data/test/templates/with_bom.haml +1 -0
  73. data/test/temple_line_counter_test.rb +40 -0
  74. data/test/test_helper.rb +26 -8
  75. data/test/util_test.rb +6 -47
  76. metadata +53 -36
  77. data/test/gemfiles/Gemfile.rails-3.0.x +0 -5
  78. data/test/gemfiles/Gemfile.rails-3.1.x +0 -6
  79. data/test/gemfiles/Gemfile.rails-3.2.x +0 -5
  80. data/test/templates/_av_partial_1_ugly.haml +0 -9
  81. data/test/templates/_av_partial_2_ugly.haml +0 -5
  82. data/test/templates/action_view_ugly.haml +0 -47
  83. 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,17 +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
87
  LITERAL_VALUE_REGEX = /:(\w*)|(["'])((?!\\|\#\{|\#@|\#\$|\2).|\\.)*\2/
87
88
 
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
89
+ ID_KEY = 'id'.freeze
90
+ CLASS_KEY = 'class'.freeze
91
+
92
+ def initialize(options)
93
+ @options = Options.wrap(options)
94
94
  # Record the indent levels of "if" statements to validate the subsequent
95
95
  # elsif and else statements are indented at the appropriate level.
96
96
  @script_level_stack = []
@@ -98,15 +98,27 @@ module Haml
98
98
  @template_tabs = 0
99
99
  end
100
100
 
101
- 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
+
102
111
  @root = @parent = ParseNode.new(:root)
103
- @haml_comment = false
112
+ @flat = false
113
+ @filter_buffer = nil
104
114
  @indentation = nil
105
115
  @line = next_line
106
116
 
107
117
  raise SyntaxError.new(Error.message(:indenting_at_start), @line.index) if @line.tabs != 0
108
118
 
109
- while next_line
119
+ loop do
120
+ next_line
121
+
110
122
  process_indent(@line) unless @line.text.empty?
111
123
 
112
124
  if flat?
@@ -118,61 +130,66 @@ module Haml
118
130
  end
119
131
 
120
132
  @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)
133
+ process_line(@line) unless @line.text.empty?
134
+ if block_opened? || @tab_up
123
135
  @template_tabs += 1
124
136
  @parent = @parent.children.last
125
137
  end
126
138
 
127
- if !@haml_comment && !flat? && @next_line.tabs - @line.tabs > 1
139
+ if !flat? && @next_line.tabs - @line.tabs > 1
128
140
  raise SyntaxError.new(Error.message(:deeper_indenting, @next_line.tabs - @line.tabs), @next_line.index)
129
141
  end
130
142
 
131
143
  @line = @next_line
132
144
  end
133
-
134
145
  # Close all the open tags
135
146
  close until @parent.type == :root
136
147
  @root
137
148
  rescue Haml::Error => e
138
- 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}"
139
150
  raise
140
151
  end
141
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
142
177
 
143
178
  private
144
179
 
145
180
  # @private
146
- class Line < Struct.new(:text, :unstripped, :full, :index, :compiler, :eod)
181
+ class Line < Struct.new(:whitespace, :text, :full, :index, :parser, :eod)
147
182
  alias_method :eod?, :eod
148
183
 
149
184
  # @private
150
185
  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}/
186
+ @tabs ||= parser.compute_tabs(self)
187
+ end
169
188
 
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
189
+ def strip!(from)
190
+ self.text = text[from..-1]
191
+ self.text.lstrip!
192
+ self
176
193
  end
177
194
  end
178
195
 
@@ -184,9 +201,31 @@ module Haml
184
201
  end
185
202
 
186
203
  def inspect
187
- text = "(#{type} #{value.inspect}"
188
- children.each {|c| text << "\n" << c.inspect.gsub(/^/, " ")}
189
- 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, '')
190
229
  end
191
230
  end
192
231
 
@@ -195,44 +234,55 @@ module Haml
195
234
  return unless line.tabs <= @template_tabs && @template_tabs > 0
196
235
 
197
236
  to_close = @template_tabs - line.tabs
198
- 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))
199
246
  end
200
247
 
201
248
  # Processes a single line of Haml.
202
249
  #
203
250
  # This method doesn't return anything; it simply processes the line and
204
251
  # 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)
252
+ def process_line(line)
253
+ case line.text[0]
254
+ when DIV_CLASS; push div(line)
210
255
  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)
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)
215
260
  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)
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)
221
266
  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)
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)
227
275
  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)
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)
236
286
  end
237
287
  end
238
288
 
@@ -241,52 +291,47 @@ module Haml
241
291
  keyword[0] || keyword[1]
242
292
  end
243
293
 
244
- def mid_block_keyword?(text)
245
- MID_BLOCK_KEYWORDS.include?(block_keyword(text))
246
- end
247
-
248
294
  def push(node)
249
295
  @parent.children << node
250
296
  node.parent = @parent
251
297
  end
252
298
 
253
- def plain(text, escape_html = nil)
299
+ def plain(line, escape_html = nil)
254
300
  if block_opened?
255
301
  raise SyntaxError.new(Error.message(:illegal_nesting_plain), @next_line.index)
256
302
  end
257
303
 
258
- unless contains_interpolation?(text)
259
- return ParseNode.new(:plain, @index, :text => text)
304
+ unless contains_interpolation?(line.text)
305
+ return ParseNode.new(:plain, line.index + 1, :text => line.text)
260
306
  end
261
307
 
262
- escape_html = @options[:escape_html] if escape_html.nil?
263
- 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)
264
311
  end
265
312
 
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?
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?
270
317
 
271
- keyword = block_keyword(text)
318
+ keyword = block_keyword(line.text)
272
319
  check_push_script_stack(keyword)
273
320
 
274
- ParseNode.new(:script, @index, :text => text, :escape_html => escape_html,
321
+ ParseNode.new(:script, line.index + 1, :text => line.text, :escape_html => escape_html,
275
322
  :preserve => preserve, :keyword => keyword)
276
323
  end
277
324
 
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)
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)
281
328
  end
282
329
 
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"
330
+ def silent_script(line)
331
+ raise SyntaxError.new(Error.message(:no_end), line.index) if line.text[1..-1].strip == 'end'
287
332
 
288
- text = handle_ruby_multiline(text)
289
- keyword = block_keyword(text)
333
+ line = handle_ruby_multiline(line)
334
+ keyword = block_keyword(line.text)
290
335
 
291
336
  check_push_script_stack(keyword)
292
337
 
@@ -308,8 +353,8 @@ module Haml
308
353
  end
309
354
  end
310
355
 
311
- ParseNode.new(:silent_script, @index,
312
- :text => text[1..-1], :keyword => keyword)
356
+ ParseNode.new(:silent_script, @line.index + 1,
357
+ :text => line.text[1..-1], :keyword => keyword)
313
358
  end
314
359
 
315
360
  def check_push_script_stack(keyword)
@@ -323,18 +368,25 @@ module Haml
323
368
  end
324
369
 
325
370
  def haml_comment(text)
326
- @haml_comment = block_opened?
327
- 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)
328
381
  end
329
382
 
330
383
  def tag(line)
331
384
  tag_name, attributes, attributes_hashes, object_ref, nuke_outer_whitespace,
332
- nuke_inner_whitespace, action, value, last_line = parse_tag(line)
385
+ nuke_inner_whitespace, action, value, last_line = parse_tag(line.text)
333
386
 
334
- preserve_tag = @options[:preserve].include?(tag_name)
387
+ preserve_tag = @options.preserve.include?(tag_name)
335
388
  nuke_inner_whitespace ||= preserve_tag
336
- preserve_tag = false if @options[:ugly]
337
- escape_html = (action == '&' || (action != '!' && @options[:escape_html]))
389
+ escape_html = (action == '&' || (action != '!' && @options.escape_html))
338
390
 
339
391
  case action
340
392
  when '/'; self_closing = true
@@ -369,22 +421,20 @@ module Haml
369
421
  end
370
422
 
371
423
  attributes = Parser.parse_class_and_id(attributes)
372
- attributes_list = []
424
+ dynamic_attributes = DynamicAttributes.new
373
425
 
374
426
  if attributes_hashes[:new]
375
427
  static_attributes, attributes_hash = attributes_hashes[:new]
376
- Buffer.merge_attrs(attributes, static_attributes) if static_attributes
377
- attributes_list << attributes_hash
428
+ AttributeBuilder.merge_attributes!(attributes, static_attributes) if static_attributes
429
+ dynamic_attributes.new = attributes_hash
378
430
  end
379
431
 
380
432
  if attributes_hashes[:old]
381
433
  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]
434
+ AttributeBuilder.merge_attributes!(attributes, static_attributes) if static_attributes
435
+ dynamic_attributes.old = attributes_hashes[:old] unless static_attributes || @options.suppress_eval
384
436
  end
385
437
 
386
- attributes_list.compact!
387
-
388
438
  raise SyntaxError.new(Error.message(:illegal_nesting_self_closing), @next_line.index) if block_opened? && self_closing
389
439
  raise SyntaxError.new(Error.message(:no_ruby_code, action), last_line - 1) if parse && value.empty?
390
440
  raise SyntaxError.new(Error.message(:self_closing_content), last_line - 1) if self_closing && !value.empty?
@@ -393,56 +443,70 @@ module Haml
393
443
  raise SyntaxError.new(Error.message(:illegal_nesting_line, tag_name), @next_line.index)
394
444
  end
395
445
 
396
- 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})
397
447
  value = nil if value.empty? && (block_opened? || self_closing)
398
- value = handle_ruby_multiline(value) if parse
448
+ line.text = value
449
+ line = handle_ruby_multiline(line) if parse
399
450
 
400
- ParseNode.new(:tag, @index, :name => tag_name, :attributes => attributes,
401
- :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,
402
453
  :nuke_inner_whitespace => nuke_inner_whitespace,
403
454
  :nuke_outer_whitespace => nuke_outer_whitespace, :object_ref => object_ref,
404
455
  :escape_html => escape_html, :preserve_tag => preserve_tag,
405
- :preserve_script => preserve_script, :parse => parse, :value => value)
456
+ :preserve_script => preserve_script, :parse => parse, :value => line.text)
406
457
  end
407
458
 
408
459
  # Renders a line that creates an XHTML tag and has an implicit div because of
409
460
  # `.` or `#`.
410
461
  def div(line)
411
- tag('%div' + line)
462
+ line.text = "%div#{line.text}"
463
+ tag(line)
412
464
  end
413
465
 
414
466
  # Renders an XHTML comment.
415
- def comment(line)
416
- conditional, line = balance(line, ?[, ?]) if line[0] == ?[
417
- line.strip!
418
- 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
419
474
 
420
- if block_opened? && !line.empty?
475
+ conditional, text = balance(text, ?[, ?]) if text[0] == ?[
476
+ text.strip!
477
+
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?
421
486
  raise SyntaxError.new(Haml::Error.message(:illegal_nesting_content), @next_line.index)
422
487
  end
423
488
 
424
- ParseNode.new(:comment, @index, :conditional => conditional, :text => line)
489
+ ParseNode.new(:comment, @line.index + 1, :conditional => conditional, :text => text, :revealed => revealed, :parse => parse)
425
490
  end
426
491
 
427
492
  # Renders an XHTML doctype or XML shebang.
428
- def doctype(line)
493
+ def doctype(text)
429
494
  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)
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)
432
497
  end
433
498
 
434
499
  def filter(name)
435
500
  raise Error.new(Error.message(:invalid_filter_name, name)) unless name =~ /^\w+$/
436
501
 
437
- @filter_buffer = String.new
438
-
439
502
  if filter_opened?
440
503
  @flat = true
504
+ @filter_buffer = String.new
441
505
  # If we don't know the indentation by now, it'll be set in Line#tabs
442
506
  @flat_spaces = @indentation * (@template_tabs+1) if @indentation
443
507
  end
444
508
 
445
- ParseNode.new(:filter, @index, :name => name, :text => @filter_buffer)
509
+ ParseNode.new(:filter, @line.index + 1, :name => name, :text => @filter_buffer)
446
510
  end
447
511
 
448
512
  def close
@@ -452,13 +516,17 @@ module Haml
452
516
  end
453
517
 
454
518
  def close_filter(_)
455
- @flat = false
456
- @flat_spaces = nil
457
- @filter_buffer = nil
519
+ close_flat_section
458
520
  end
459
521
 
460
522
  def close_haml_comment(_)
461
- @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
462
530
  end
463
531
 
464
532
  def close_silent_script(node)
@@ -485,23 +553,33 @@ module Haml
485
553
  # that can then be merged with another attributes hash.
486
554
  def self.parse_class_and_id(list)
487
555
  attributes = {}
488
- 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|
489
559
  case type
490
560
  when '.'
491
- if attributes['class']
492
- attributes['class'] += " "
561
+ if attributes[CLASS_KEY]
562
+ attributes[CLASS_KEY] += " "
493
563
  else
494
- attributes['class'] = ""
564
+ attributes[CLASS_KEY] = ""
495
565
  end
496
- attributes['class'] += property
497
- when '#'; attributes['id'] = property
566
+ attributes[CLASS_KEY] += property
567
+ when '#'; attributes[ID_KEY] = property
498
568
  end
499
569
  end
500
570
  attributes
501
571
  end
502
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
503
578
  def parse_static_hash(text)
504
579
  attributes = {}
580
+ return attributes if text.empty?
581
+
582
+ text = text[1...-1] # strip brackets
505
583
  scanner = StringScanner.new(text)
506
584
  scanner.scan(/\s+/)
507
585
  until scanner.eos?
@@ -515,20 +593,20 @@ module Haml
515
593
  end
516
594
 
517
595
  # 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
596
+ def parse_tag(text)
597
+ match = text.scan(/%([-:\w]+)([-:\w.#\@]*)(.+)?/)[0]
598
+ raise SyntaxError.new(Error.message(:invalid_tag, text)) unless match
521
599
 
522
600
  tag_name, attributes, rest = match
523
601
 
524
- if attributes =~ /[\.#](\.|#|\z)/
602
+ if !attributes.empty? && (attributes =~ /[.#](\.|#|\z)/)
525
603
  raise SyntaxError.new(Error.message(:illegal_element))
526
604
  end
527
605
 
528
606
  new_attributes_hash = old_attributes_hash = last_line = nil
529
- object_ref = "nil"
607
+ object_ref = :nil
530
608
  attributes_hashes = {}
531
- while rest
609
+ while rest && !rest.empty?
532
610
  case rest[0]
533
611
  when ?{
534
612
  break if old_attributes_hash
@@ -539,38 +617,46 @@ module Haml
539
617
  new_attributes_hash, rest, last_line = parse_new_attributes(rest)
540
618
  attributes_hashes[:new] = new_attributes_hash
541
619
  when ?[
542
- break unless object_ref == "nil"
620
+ break unless object_ref == :nil
543
621
  object_ref, rest = balance(rest, ?[, ?])
544
622
  else; break
545
623
  end
546
624
  end
547
625
 
548
- if rest
626
+ if rest && !rest.empty?
549
627
  nuke_whitespace, action, value = rest.scan(/(<>|><|[><])?([=\/\~&!])?(.*)?/)[0]
550
- nuke_whitespace ||= ''
551
- nuke_outer_whitespace = nuke_whitespace.include? '>'
552
- 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
553
632
  end
554
633
 
555
- if @options[:remove_whitespace]
634
+ if @options.remove_whitespace
556
635
  nuke_outer_whitespace = true
557
636
  nuke_inner_whitespace = true
558
637
  end
559
638
 
560
- value = value.to_s.strip
639
+ if value.nil?
640
+ value = ''
641
+ else
642
+ value.strip!
643
+ end
561
644
  [tag_name, attributes, attributes_hashes, object_ref, nuke_outer_whitespace,
562
- nuke_inner_whitespace, action, value, last_line || @index]
645
+ nuke_inner_whitespace, action, value, last_line || @line.index + 1]
563
646
  end
564
647
 
565
- def parse_old_attributes(line)
566
- line = line.dup
567
- 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
568
654
 
569
655
  begin
570
- attributes_hash, rest = balance(line, ?{, ?})
656
+ attributes_hash, rest = balance(text, ?{, ?})
571
657
  rescue SyntaxError => e
572
- if line.strip[-1] == ?, && e.message == Error.message(:unbalanced_brackets)
573
- line << "\n" << @next_line.text
658
+ if text.strip[-1] == ?, && e.message == Error.message(:unbalanced_brackets)
659
+ text << "\n#{@next_line.text}"
574
660
  last_line += 1
575
661
  next_line
576
662
  retry
@@ -579,14 +665,15 @@ module Haml
579
665
  raise e
580
666
  end
581
667
 
582
- attributes_hash = attributes_hash[1...-1] if attributes_hash
583
668
  return attributes_hash, rest, last_line
584
669
  end
585
670
 
586
- def parse_new_attributes(line)
587
- line = line.dup
588
- scanner = StringScanner.new(line)
589
- 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
590
677
  attributes = {}
591
678
 
592
679
  scanner.scan(/\(\s*/)
@@ -595,14 +682,15 @@ module Haml
595
682
  break if name.nil?
596
683
 
597
684
  if name == false
598
- text = (Haml::Util.balance(line, ?(, ?)) || [line]).first
685
+ scanned = Haml::Util.balance(text, ?(, ?))
686
+ text = scanned ? scanned.first : text
599
687
  raise Haml::SyntaxError.new(Error.message(:invalid_attribute_list, text.inspect), last_line - 1)
600
688
  end
601
689
  attributes[name] = value
602
690
  scanner.scan(/\s*/)
603
691
 
604
692
  if scanner.eos?
605
- line << " " << @next_line.text
693
+ text << " #{@next_line.text}"
606
694
  last_line += 1
607
695
  next_line
608
696
  scanner.scan(/\s*/)
@@ -615,7 +703,7 @@ module Haml
615
703
  if type == :static
616
704
  static_attributes[name] = val
617
705
  else
618
- dynamic_attributes << inspect_obj(name) << " => " << val << ","
706
+ dynamic_attributes << "#{inspect_obj(name)} => #{val},"
619
707
  end
620
708
  end
621
709
  dynamic_attributes << "}"
@@ -650,35 +738,16 @@ module Haml
650
738
 
651
739
  return name, [:static, content.first[1]] if content.size == 1
652
740
  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
741
+ %!"#{content.each_with_object('') {|(t, v), s| s << (t == :str ? inspect_obj(v)[1...-1] : "\#{#{v}}")}}"!]
664
742
  end
665
743
 
666
744
  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
745
+ line = @template.shift || raise(StopIteration)
677
746
 
678
747
  # `flat?' here is a little outdated,
679
748
  # so we have to manually check if either the previous or current line
680
749
  # closes the flat block, as well as whether a new block is opened.
681
- line_defined = instance_variable_defined?('@line')
750
+ line_defined = instance_variable_defined?(:@line)
682
751
  @line.tabs if line_defined
683
752
  unless (flat? && !closes_flat?(line) && !closes_flat?(@line)) ||
684
753
  (line_defined && @line.text[0] == ?: && line.full =~ %r[^#{@line.full[/^\s+/]}\s])
@@ -694,21 +763,17 @@ module Haml
694
763
  line && !line.text.empty? && line.full !~ /^#{@flat_spaces}/
695
764
  end
696
765
 
697
- def un_next_line(line)
698
- @template.unshift line
699
- @template_index -= 1
700
- end
701
-
702
766
  def handle_multiline(line)
703
767
  return unless is_multiline?(line.text)
704
768
  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]
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
710
776
  end
711
- un_next_line new_line
712
777
  end
713
778
 
714
779
  # Checks whether or not `line` is in a multiline sequence.
@@ -716,18 +781,18 @@ module Haml
716
781
  text && text.length > 1 && text[-1] == MULTILINE_CHAR_VALUE && text[-2] == ?\s && text !~ BLOCK_WITH_SPACES
717
782
  end
718
783
 
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
784
+ def handle_ruby_multiline(line)
785
+ line.text.rstrip!
786
+ return line unless is_ruby_multiline?(line.text)
723
787
  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)
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)
729
794
  next_line
730
- text
795
+ line
731
796
  end
732
797
 
733
798
  # `text' is a Ruby multiline block if it:
@@ -735,16 +800,13 @@ module Haml
735
800
  # - but not "?," which is a character literal
736
801
  # (however, "x?," is a method call and not a literal)
737
802
  # - and not "?\," which is a character literal
738
- #
739
803
  def is_ruby_multiline?(text)
740
804
  text && text.length > 1 && text[-1] == ?, &&
741
- !((text[-3..-2] =~ /\W\?/) || text[-3..-2] == "?\\")
805
+ !((text[-3, 2] =~ /\W\?/) || text[-3, 2] == "?\\")
742
806
  end
743
807
 
744
808
  def balance(*args)
745
- res = Haml::Util.balance(*args)
746
- return res if res
747
- raise SyntaxError.new(Error.message(:unbalanced_brackets))
809
+ Haml::Util.balance(*args) or raise(SyntaxError.new(Error.message(:unbalanced_brackets)))
748
810
  end
749
811
 
750
812
  def block_opened?
@@ -754,7 +816,7 @@ module Haml
754
816
  # Same semantics as block_opened?, except that block_opened? uses Line#tabs,
755
817
  # which doesn't interact well with filter lines
756
818
  def filter_opened?
757
- @next_line.full =~ (@indentation ? /^#{@indentation * @template_tabs}/ : /^\s/)
819
+ @next_line.full =~ (@indentation ? /^#{@indentation * (@template_tabs + 1)}/ : /^\s/)
758
820
  end
759
821
 
760
822
  def flat?