haml 4.0.7 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +1 -1
  3. data/CHANGELOG.md +42 -4
  4. data/FAQ.md +4 -14
  5. data/MIT-LICENSE +1 -1
  6. data/README.md +85 -42
  7. data/REFERENCE.md +108 -57
  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 +22 -132
  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 -43
  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/haml-spec/LICENSE +0 -14
  81. data/test/haml-spec/README.md +0 -106
  82. data/test/haml-spec/lua_haml_spec.lua +0 -38
  83. data/test/haml-spec/perl_haml_test.pl +0 -81
  84. data/test/haml-spec/ruby_haml_test.rb +0 -23
  85. data/test/haml-spec/tests.json +0 -660
  86. data/test/templates/_av_partial_1_ugly.haml +0 -9
  87. data/test/templates/_av_partial_2_ugly.haml +0 -5
  88. data/test/templates/action_view_ugly.haml +0 -47
  89. 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?