haml 4.0.0 → 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 (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?