haml 2.0.10 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of haml might be problematic. Click here for more details.

Files changed (107) hide show
  1. data/.yardopts +5 -0
  2. data/MIT-LICENSE +1 -1
  3. data/README.md +347 -0
  4. data/Rakefile +124 -19
  5. data/VERSION +1 -1
  6. data/VERSION_NAME +1 -0
  7. data/extra/haml-mode.el +397 -78
  8. data/extra/sass-mode.el +148 -36
  9. data/extra/update_watch.rb +13 -0
  10. data/lib/haml.rb +15 -993
  11. data/lib/haml/buffer.rb +131 -84
  12. data/lib/haml/engine.rb +129 -97
  13. data/lib/haml/error.rb +7 -7
  14. data/lib/haml/exec.rb +127 -42
  15. data/lib/haml/filters.rb +107 -42
  16. data/lib/haml/helpers.rb +210 -156
  17. data/lib/haml/helpers/action_view_extensions.rb +34 -39
  18. data/lib/haml/helpers/action_view_mods.rb +132 -139
  19. data/lib/haml/html.rb +77 -65
  20. data/lib/haml/precompiler.rb +404 -213
  21. data/lib/haml/shared.rb +78 -0
  22. data/lib/haml/template.rb +14 -14
  23. data/lib/haml/template/patch.rb +2 -2
  24. data/lib/haml/template/plugin.rb +2 -3
  25. data/lib/haml/util.rb +211 -6
  26. data/lib/haml/version.rb +30 -13
  27. data/lib/sass.rb +7 -856
  28. data/lib/sass/css.rb +169 -161
  29. data/lib/sass/engine.rb +344 -328
  30. data/lib/sass/environment.rb +79 -0
  31. data/lib/sass/error.rb +33 -11
  32. data/lib/sass/files.rb +139 -0
  33. data/lib/sass/plugin.rb +160 -117
  34. data/lib/sass/plugin/merb.rb +7 -6
  35. data/lib/sass/plugin/rails.rb +5 -6
  36. data/lib/sass/repl.rb +58 -0
  37. data/lib/sass/script.rb +59 -0
  38. data/lib/sass/script/bool.rb +17 -0
  39. data/lib/sass/script/color.rb +183 -0
  40. data/lib/sass/script/funcall.rb +50 -0
  41. data/lib/sass/script/functions.rb +198 -0
  42. data/lib/sass/script/lexer.rb +178 -0
  43. data/lib/sass/script/literal.rb +177 -0
  44. data/lib/sass/script/node.rb +14 -0
  45. data/lib/sass/script/number.rb +381 -0
  46. data/lib/sass/script/operation.rb +45 -0
  47. data/lib/sass/script/parser.rb +172 -0
  48. data/lib/sass/script/string.rb +12 -0
  49. data/lib/sass/script/unary_operation.rb +34 -0
  50. data/lib/sass/script/variable.rb +31 -0
  51. data/lib/sass/tree/comment_node.rb +73 -10
  52. data/lib/sass/tree/debug_node.rb +30 -0
  53. data/lib/sass/tree/directive_node.rb +42 -17
  54. data/lib/sass/tree/file_node.rb +41 -0
  55. data/lib/sass/tree/for_node.rb +48 -0
  56. data/lib/sass/tree/if_node.rb +54 -0
  57. data/lib/sass/tree/mixin_def_node.rb +29 -0
  58. data/lib/sass/tree/mixin_node.rb +48 -0
  59. data/lib/sass/tree/node.rb +214 -11
  60. data/lib/sass/tree/prop_node.rb +109 -0
  61. data/lib/sass/tree/rule_node.rb +178 -51
  62. data/lib/sass/tree/variable_node.rb +34 -0
  63. data/lib/sass/tree/while_node.rb +31 -0
  64. data/test/haml/engine_test.rb +331 -36
  65. data/test/haml/helper_test.rb +12 -1
  66. data/test/haml/results/content_for_layout.xhtml +0 -3
  67. data/test/haml/results/filters.xhtml +2 -0
  68. data/test/haml/results/list.xhtml +1 -1
  69. data/test/haml/template_test.rb +7 -2
  70. data/test/haml/templates/content_for_layout.haml +0 -2
  71. data/test/haml/templates/list.haml +1 -1
  72. data/test/haml/util_test.rb +92 -0
  73. data/test/sass/css2sass_test.rb +69 -24
  74. data/test/sass/engine_test.rb +586 -64
  75. data/test/sass/functions_test.rb +125 -0
  76. data/test/sass/more_results/more1.css +9 -0
  77. data/test/sass/more_results/more1_with_line_comments.css +26 -0
  78. data/test/sass/more_results/more_import.css +29 -0
  79. data/test/sass/more_templates/_more_partial.sass +2 -0
  80. data/test/sass/more_templates/more1.sass +23 -0
  81. data/test/sass/more_templates/more_import.sass +11 -0
  82. data/test/sass/plugin_test.rb +81 -28
  83. data/test/sass/results/line_numbers.css +49 -0
  84. data/test/sass/results/{constants.css → script.css} +4 -4
  85. data/test/sass/results/subdir/subdir.css +2 -0
  86. data/test/sass/results/units.css +11 -0
  87. data/test/sass/script_test.rb +258 -0
  88. data/test/sass/templates/import.sass +1 -1
  89. data/test/sass/templates/importee.sass +7 -2
  90. data/test/sass/templates/line_numbers.sass +13 -0
  91. data/test/sass/templates/{constants.sass → script.sass} +11 -10
  92. data/test/sass/templates/subdir/nested_subdir/_nested_partial.sass +2 -0
  93. data/test/sass/templates/subdir/subdir.sass +2 -2
  94. data/test/sass/templates/units.sass +11 -0
  95. data/test/test_helper.rb +14 -0
  96. metadata +77 -19
  97. data/FAQ +0 -138
  98. data/README.rdoc +0 -319
  99. data/lib/sass/constant.rb +0 -216
  100. data/lib/sass/constant/color.rb +0 -101
  101. data/lib/sass/constant/literal.rb +0 -54
  102. data/lib/sass/constant/nil.rb +0 -9
  103. data/lib/sass/constant/number.rb +0 -87
  104. data/lib/sass/constant/operation.rb +0 -30
  105. data/lib/sass/constant/string.rb +0 -22
  106. data/lib/sass/tree/attr_node.rb +0 -57
  107. data/lib/sass/tree/value_node.rb +0 -20
@@ -1,7 +1,12 @@
1
1
  require 'strscan'
2
+ require 'haml/shared'
2
3
 
3
4
  module Haml
5
+ # Handles the internal pre-compilation from Haml into Ruby code,
6
+ # which then runs the final creation of the HTML string.
4
7
  module Precompiler
8
+ include Haml::Util
9
+
5
10
  # Designates an XHTML/XML element.
6
11
  ELEMENT = ?%
7
12
 
@@ -61,13 +66,10 @@ module Haml
61
66
  # of a multiline string.
62
67
  MULTILINE_CHAR_VALUE = ?|
63
68
 
64
- # Characters that designate that a multiline string may be about
65
- # to begin.
66
- MULTILINE_STARTERS = SPECIAL_CHARACTERS - [?/]
67
-
68
- # Keywords that appear in the middle of a Ruby block with lowered
69
- # indentation. If a block has been started using indentation,
70
- # lowering the indentation with one of these won't end the block.
69
+ # Regex to match keywords that appear in the middle of a Ruby block
70
+ # with lowered indentation.
71
+ # If a block has been started using indentation,
72
+ # lowering the indentation with one of these won't end the block.
71
73
  # For example:
72
74
  #
73
75
  # - if foo
@@ -77,7 +79,7 @@ module Haml
77
79
  #
78
80
  # The block is ended after <tt>%p no!</tt>, because <tt>else</tt>
79
81
  # is a member of this array.
80
- MID_BLOCK_KEYWORDS = ['else', 'elsif', 'rescue', 'ensure', 'when']
82
+ MID_BLOCK_KEYWORD_REGEX = /-\s*(#{%w[else elsif rescue ensure when end].join('|')})\b/
81
83
 
82
84
  # The Regex that matches a Doctype command.
83
85
  DOCTYPE_REGEX = /(\d\.\d)?[\s]*([a-z]*)/i
@@ -99,7 +101,7 @@ END
99
101
  @haml_buffer = @haml_buffer.upper
100
102
  _erbout
101
103
  END
102
- preamble + locals_code(local_names) + @precompiled + postamble
104
+ preamble + locals_code(local_names) + precompiled + postamble
103
105
  end
104
106
 
105
107
  def locals_code(names)
@@ -113,57 +115,65 @@ END
113
115
  end.join(';') + ';'
114
116
  end
115
117
 
116
- Line = Struct.new(:text, :unstripped, :index, :spaces, :tabs)
118
+ class Line < Struct.new(:text, :unstripped, :full, :index, :precompiler, :eod)
119
+ alias_method :eod?, :eod
117
120
 
118
- def precompile
119
- @haml_comment = @dont_indent_next_line = @dont_tab_up_next_text = false
120
- @indentation = nil
121
- old_line = Line.new
122
- @template.split(/\r\n|\r|\n/).each_with_index do |text, index|
123
- @next_line = line = Line.new(text.strip, text.lstrip.chomp, index)
124
- line.spaces, line.tabs = count_soft_tabs(text)
121
+ def tabs
122
+ line = self
123
+ @tabs ||= precompiler.instance_eval do
124
+ break 0 if line.text.empty? || !(whitespace = line.full[/^\s+/])
125
125
 
126
- suppress_render = handle_multiline(old_line) unless flat?
126
+ if @indentation.nil?
127
+ @indentation = whitespace
127
128
 
128
- if old_line.text.nil? || suppress_render
129
- old_line = line
130
- resolve_newlines
131
- newline
132
- next
133
- end
129
+ if @indentation.include?(?\s) && @indentation.include?(?\t)
130
+ raise SyntaxError.new("Indentation can't use both tabs and spaces.", line.index)
131
+ end
132
+
133
+ @flat_spaces = @indentation * @template_tabs if flat?
134
+ break 1
135
+ end
134
136
 
135
- process_indent(old_line) unless old_line.text.empty?
137
+ tabs = whitespace.length / @indentation.length
138
+ break tabs if whitespace == @indentation * tabs
139
+ break @template_tabs if flat? && whitespace =~ /^#{@indentation * @template_tabs}/
136
140
 
137
- if line.text.empty? && !flat?
138
- newline
139
- next
141
+ raise SyntaxError.new(<<END.strip.gsub("\n", ' '), line.index)
142
+ Inconsistent indentation: #{Haml::Shared.human_indentation whitespace, true} used for indentation,
143
+ but the rest of the document was indented using #{Haml::Shared.human_indentation @indentation}.
144
+ END
140
145
  end
146
+ end
147
+ end
148
+
149
+ def precompile
150
+ @haml_comment = @dont_indent_next_line = @dont_tab_up_next_text = false
151
+ @indentation = nil
152
+ @line = next_line
153
+ resolve_newlines
154
+ newline
155
+
156
+ raise SyntaxError.new("Indenting at the beginning of the document is illegal.", @line.index) if @line.tabs != 0
157
+
158
+ while next_line
159
+ process_indent(@line) unless @line.text.empty?
141
160
 
142
161
  if flat?
143
- push_flat(old_line)
144
- old_line = line
162
+ push_flat(@line)
163
+ @line = @next_line
145
164
  newline
146
165
  next
147
166
  end
148
167
 
149
- if old_line.spaces != old_line.tabs * 2
150
- raise SyntaxError.new(<<END.strip, old_line.index)
151
- #{old_line.spaces} space#{old_line.spaces == 1 ? ' was' : 's were'} used for indentation. Haml must be indented using two spaces.
152
- END
153
- end
168
+ process_line(@line.text, @line.index) unless @line.text.empty? || @haml_comment
154
169
 
155
- unless old_line.text.empty? || @haml_comment
156
- process_line(old_line.text, old_line.index, line.tabs > old_line.tabs && !line.text.empty?)
170
+ if !flat? && @next_line.tabs - @line.tabs > 1
171
+ raise SyntaxError.new("The line was indented #{@next_line.tabs - @line.tabs} levels deeper than the previous line.", @next_line.index)
157
172
  end
158
- resolve_newlines
159
173
 
160
- if !flat? && line.tabs - old_line.tabs > 1
161
- raise SyntaxError.new(<<END.strip, line.index)
162
- #{line.spaces} spaces were used for indentation. Haml must be indented using two spaces.
163
- END
164
- end
165
- old_line = line
166
- newline
174
+ resolve_newlines unless @next_line.eod?
175
+ @line = @next_line
176
+ newline unless @next_line.eod?
167
177
  end
168
178
 
169
179
  # Close all the open tags
@@ -183,22 +193,25 @@ END
183
193
  #
184
194
  # This method doesn't return anything; it simply processes the line and
185
195
  # adds the appropriate code to <tt>@precompiled</tt>.
186
- def process_line(text, index, block_opened)
187
- @block_opened = block_opened
196
+ def process_line(text, index)
188
197
  @index = index + 1
189
198
 
190
199
  case text[0]
191
- when DIV_CLASS, DIV_ID; render_div(text)
200
+ when DIV_CLASS; render_div(text)
201
+ when DIV_ID
202
+ return push_plain(text) if text[1] == ?{
203
+ render_div(text)
192
204
  when ELEMENT; render_tag(text)
193
205
  when COMMENT; render_comment(text[1..-1].strip)
194
206
  when SANITIZE
195
- return push_script(unescape_interpolation(text[3..-1].strip), false, false, false, true) if text[1..2] == "=="
196
- return push_script(text[2..-1].strip, false, false, false, true) if text[1] == SCRIPT
207
+ return push_script(unescape_interpolation(text[3..-1].strip), :escape_html => true) if text[1..2] == "=="
208
+ return push_script(text[2..-1].strip, :escape_html => true) if text[1] == SCRIPT
209
+ return push_script(unescape_interpolation(text[1..-1].strip), :escape_html => true) if text[1] == ?\s
197
210
  push_plain text
198
211
  when SCRIPT
199
- return push_script(unescape_interpolation(text[2..-1].strip), false) if text[1] == SCRIPT
200
- return push_script(text[1..-1], false, false, false, true) if options[:escape_html]
201
- push_script(text[1..-1], false)
212
+ return push_script(unescape_interpolation(text[2..-1].strip)) if text[1] == SCRIPT
213
+ return push_script(text[1..-1], :escape_html => true) if options[:escape_html]
214
+ push_script(text[1..-1])
202
215
  when FLAT_SCRIPT; push_flat_script(text[1..-1])
203
216
  when SILENT_SCRIPT
204
217
  return start_haml_comment if text[1] == SILENT_COMMENT
@@ -214,15 +227,19 @@ END
214
227
  push_silent(text[1..-1], true)
215
228
  newline_now
216
229
 
217
- case_stmt = text[1..-1].split(' ', 2)[0] == "case"
218
- block = @block_opened && !mid_block_keyword?(text)
230
+ # Handle stuff like - end.join("|")
231
+ @to_close_stack.first << false if text =~ /-\s*end\b/ && !block_opened?
232
+
233
+ case_stmt = text =~ /-\s*case\b/
234
+ block = block_opened? && !mid_block_keyword?(text)
219
235
  push_and_tabulate([:script]) if block || case_stmt
220
- push_and_tabulate(nil) if block && case_stmt
236
+ push_and_tabulate(:nil) if block && case_stmt
221
237
  when FILTER; start_filtered(text[1..-1].downcase)
222
238
  when DOCTYPE
223
239
  return render_doctype(text) if text[0...3] == '!!!'
224
- return push_script(unescape_interpolation(text[3..-1].strip), false) if text[1..2] == "=="
225
- return push_script(text[2..-1].strip, false) if text[1] == SCRIPT
240
+ return push_script(unescape_interpolation(text[3..-1].strip)) if text[1..2] == "=="
241
+ return push_script(text[2..-1].strip) if text[1] == SCRIPT
242
+ return push_script(unescape_interpolation(text[1..-1].strip)) if text[1] == ?\s
226
243
  push_plain text
227
244
  when ESCAPE; push_plain text[1..-1]
228
245
  else push_plain text
@@ -232,42 +249,7 @@ END
232
249
  # Returns whether or not the text is a silent script text with one
233
250
  # of Ruby's mid-block keywords.
234
251
  def mid_block_keyword?(text)
235
- text.length > 2 && text[0] == SILENT_SCRIPT && MID_BLOCK_KEYWORDS.include?(text[1..-1].split[0])
236
- end
237
-
238
- # Deals with all the logic of figuring out whether a given line is
239
- # the beginning, continuation, or end of a multiline sequence.
240
- #
241
- # This returns whether or not the line should be
242
- # rendered normally.
243
- def handle_multiline(line)
244
- text = line.text
245
-
246
- # A multiline string is active, and is being continued
247
- if is_multiline?(text) && @multiline
248
- @multiline.text << text[0...-1]
249
- return true
250
- end
251
-
252
- # A multiline string has just been activated, start adding the lines
253
- if is_multiline?(text) && (MULTILINE_STARTERS.include? text[0])
254
- @multiline = Line.new text[0...-1], nil, line.index, nil, line.tabs
255
- process_indent(line)
256
- return true
257
- end
258
-
259
- # A multiline string has just ended, make line into the result
260
- if @multiline && !line.text.empty?
261
- process_line(@multiline.text, @multiline.index, line.tabs > @multiline.tabs)
262
- @multiline = nil
263
- end
264
-
265
- return false
266
- end
267
-
268
- # Checks whether or not +line+ is in a multiline sequence.
269
- def is_multiline?(text)
270
- text && text.length > 1 && text[-1] == MULTILINE_CHAR_VALUE && text[-2] == ?\s
252
+ MID_BLOCK_KEYWORD_REGEX =~ text
271
253
  end
272
254
 
273
255
  # Evaluates <tt>text</tt> in the context of the scope object, but
@@ -281,14 +263,14 @@ END
281
263
  # Adds <tt>text</tt> to <tt>@buffer</tt> with appropriate tabulation
282
264
  # without parsing it.
283
265
  def push_merged_text(text, tab_change = 0, indent = true)
284
- @merged_text << (!indent || @dont_indent_next_line || @options[:ugly] ? text : "#{' ' * @output_tabs}#{text}")
266
+ text = !indent || @dont_indent_next_line || @options[:ugly] ? text : "#{' ' * @output_tabs}#{text}"
267
+ @to_merge << [:text, text, tab_change]
285
268
  @dont_indent_next_line = false
286
- @tab_change += tab_change
287
269
  end
288
270
 
289
271
  # Concatenate <tt>text</tt> to <tt>@buffer</tt> without tabulation.
290
272
  def concat_merged_text(text)
291
- @merged_text << text
273
+ @to_merge << [:text, text, 0]
292
274
  end
293
275
 
294
276
  def push_text(text, tab_change = 0)
@@ -296,59 +278,87 @@ END
296
278
  end
297
279
 
298
280
  def flush_merged_text
299
- return if @merged_text.empty?
281
+ return if @to_merge.empty?
300
282
 
301
- @precompiled << "_hamlout.push_text(#{@merged_text.dump}"
302
- @precompiled << ", #{@dont_tab_up_next_text.inspect}" if @dont_tab_up_next_text || @tab_change != 0
303
- @precompiled << ", #{@tab_change}" if @tab_change != 0
304
- @precompiled << ");"
305
- @merged_text = ''
283
+ text, tab_change = @to_merge.inject(["", 0]) do |(str, mtabs), (type, val, tabs)|
284
+ case type
285
+ when :text
286
+ [str << val.inspect[1...-1], mtabs + tabs]
287
+ when :script
288
+ if mtabs != 0 && !@options[:ugly]
289
+ val = "_hamlout.adjust_tabs(#{mtabs}); " + val
290
+ end
291
+ [str << "\#{#{val}}", 0]
292
+ else
293
+ raise SyntaxError.new("[HAML BUG] Undefined entry in Haml::Precompiler@to_merge.")
294
+ end
295
+ end
296
+
297
+ @precompiled <<
298
+ if @options[:ugly]
299
+ "_erbout << \"#{text}\";"
300
+ else
301
+ "_hamlout.push_text(\"#{text}\", #{tab_change}, #{@dont_tab_up_next_text.inspect});"
302
+ end
303
+ @to_merge = []
306
304
  @dont_tab_up_next_text = false
307
- @tab_change = 0
308
305
  end
309
306
 
310
307
  # Renders a block of text as plain text.
311
308
  # Also checks for an illegally opened block.
312
309
  def push_plain(text)
313
- if @block_opened
310
+ if block_opened?
314
311
  raise SyntaxError.new("Illegal nesting: nesting within plain text is illegal.", @next_line.index)
315
312
  end
316
313
 
317
- push_text text
314
+ if contains_interpolation?(text)
315
+ push_script unescape_interpolation(text)
316
+ else
317
+ push_text text
318
+ end
318
319
  end
319
320
 
320
321
  # Adds +text+ to <tt>@buffer</tt> while flattening text.
321
322
  def push_flat(line)
322
- tabulation = line.spaces - @flat_spaces
323
- tabulation = tabulation > -1 ? tabulation : 0
324
- @filter_buffer << "#{' ' * tabulation}#{line.unstripped}\n"
323
+ text = line.full.dup
324
+ text = "" unless text.gsub!(/^#{@flat_spaces}/, '')
325
+ @filter_buffer << "#{text}\n"
325
326
  end
326
327
 
327
328
  # Causes <tt>text</tt> to be evaluated in the context of
328
329
  # the scope object and the result to be added to <tt>@buffer</tt>.
329
330
  #
330
- # If <tt>preserve_script</tt> is true, Haml::Helpers#find_and_flatten is run on
331
+ # If <tt>opts[:preserve_script]</tt> is true, Haml::Helpers#find_and_flatten is run on
331
332
  # the result before it is added to <tt>@buffer</tt>
332
- def push_script(text, preserve_script, in_tag = false, preserve_tag = false,
333
- escape_html = false, nuke_inner_whitespace = false)
333
+ def push_script(text, opts = {})
334
+ raise SyntaxError.new("There's no Ruby code for = to evaluate.") if text.empty?
335
+ return if options[:suppress_eval]
336
+
337
+ args = %w[preserve_script in_tag preserve_tag escape_html nuke_inner_whitespace]
338
+ args.map! {|name| opts[name.to_sym]}
339
+ args << !block_opened? << @options[:ugly]
340
+
341
+ no_format = @options[:ugly] &&
342
+ !(opts[:preserve_script] || opts[:preserve_tag] || opts[:escape_html])
343
+ output_temp = "(haml_very_temp = haml_temp; haml_temp = nil; haml_very_temp)"
344
+ out = "_hamlout.#{static_method_name(:format_script, *args)}(#{output_temp});"
345
+
334
346
  # Prerender tabulation unless we're in a tag
335
- push_merged_text '' unless in_tag
347
+ push_merged_text '' unless opts[:in_tag]
336
348
 
337
- flush_merged_text
338
- return if options[:suppress_eval]
349
+ unless block_opened?
350
+ @to_merge << [:script, no_format ? "#{text}\n" : "haml_temp = #{text}\n#{out}"]
351
+ concat_merged_text("\n") unless opts[:in_tag] || opts[:nuke_inner_whitespace]
352
+ @newlines -= 1
353
+ return
354
+ end
339
355
 
340
- raise SyntaxError.new("There's no Ruby code for = to evaluate.") if text.empty?
356
+ flush_merged_text
341
357
 
342
358
  push_silent "haml_temp = #{text}"
343
359
  newline_now
344
- args = [preserve_script, in_tag, preserve_tag,
345
- escape_html, nuke_inner_whitespace].map { |a| a.inspect }.join(', ')
346
- out = "haml_temp = _hamlout.push_script(haml_temp, #{args});"
347
- if @block_opened
348
- push_and_tabulate([:loud, out])
349
- else
350
- @precompiled << out
351
- end
360
+ push_and_tabulate([:loud, "_erbout << #{no_format ? "#{output_temp}.to_s;" : out}",
361
+ !(opts[:in_tag] || opts[:nuke_inner_whitespace] || @options[:ugly])])
352
362
  end
353
363
 
354
364
  # Causes <tt>text</tt> to be evaluated, and Haml::Helpers#find_and_flatten
@@ -357,11 +367,11 @@ END
357
367
  flush_merged_text
358
368
 
359
369
  raise SyntaxError.new("There's no Ruby code for ~ to evaluate.") if text.empty?
360
- push_script(text, true)
370
+ push_script(text, :preserve_script => true)
361
371
  end
362
372
 
363
373
  def start_haml_comment
364
- return unless @block_opened
374
+ return unless block_opened?
365
375
 
366
376
  @haml_comment = true
367
377
  push_and_tabulate([:haml_comment])
@@ -369,21 +379,13 @@ END
369
379
 
370
380
  # Closes the most recent item in <tt>@to_close_stack</tt>.
371
381
  def close
372
- tag, value = @to_close_stack.pop
373
- case tag
374
- when :script; close_block
375
- when :comment; close_comment value
376
- when :element; close_tag value
377
- when :loud; close_loud value
378
- when :filtered; close_filtered value
379
- when :haml_comment; close_haml_comment
380
- when nil; close_nil
381
- end
382
+ tag, *rest = @to_close_stack.pop
383
+ send("close_#{tag}", *rest)
382
384
  end
383
385
 
384
386
  # Puts a line in <tt>@precompiled</tt> that will add the closing tag of
385
387
  # the most recently opened tag.
386
- def close_tag(value)
388
+ def close_element(value)
387
389
  tag, nuke_outer_whitespace, nuke_inner_whitespace = value
388
390
  @output_tabs -= 1 unless nuke_inner_whitespace
389
391
  @template_tabs -= 1
@@ -394,7 +396,7 @@ END
394
396
  end
395
397
 
396
398
  # Closes a Ruby block.
397
- def close_block
399
+ def close_script
398
400
  push_silent "end", true
399
401
  @template_tabs -= 1
400
402
  end
@@ -408,16 +410,18 @@ END
408
410
  end
409
411
 
410
412
  # Closes a loud Ruby block.
411
- def close_loud(command)
412
- push_silent 'end', true
413
+ def close_loud(command, add_newline, push_end = true)
414
+ push_silent('end', true) if push_end
413
415
  @precompiled << command
414
416
  @template_tabs -= 1
417
+ concat_merged_text("\n") if add_newline
415
418
  end
416
419
 
417
420
  # Closes a filtered block.
418
421
  def close_filtered(filter)
419
- @flat_spaces = -1
420
422
  filter.internal_compile(self, @filter_buffer)
423
+ @flat = false
424
+ @flat_spaces = nil
421
425
  @filter_buffer = nil
422
426
  @template_tabs -= 1
423
427
  end
@@ -452,8 +456,6 @@ END
452
456
  end
453
457
 
454
458
  def parse_static_hash(text)
455
- return {} unless text
456
-
457
459
  attributes = {}
458
460
  scanner = StringScanner.new(text)
459
461
  scanner.scan(/\s+/)
@@ -464,6 +466,7 @@ END
464
466
  attributes[eval(key).to_s] = eval(value).to_s
465
467
  scanner.scan(/[,\s]*/)
466
468
  end
469
+ text.count("\n").times { newline }
467
470
  attributes
468
471
  end
469
472
 
@@ -507,32 +510,130 @@ END
507
510
  def parse_tag(line)
508
511
  raise SyntaxError.new("Invalid tag: \"#{line}\".") unless match = line.scan(/%([-:\w]+)([-\w\.\#]*)(.*)/)[0]
509
512
  tag_name, attributes, rest = match
510
- attributes_hash, rest = parse_attributes(rest) if rest[0] == ?{
513
+ new_attributes_hash = old_attributes_hash = last_line = object_ref = nil
514
+ attributes_hashes = []
515
+ while rest
516
+ case rest[0]
517
+ when ?{
518
+ break if old_attributes_hash
519
+ old_attributes_hash, rest, last_line = parse_old_attributes(rest)
520
+ attributes_hashes << [:old, old_attributes_hash]
521
+ when ?(
522
+ break if new_attributes_hash
523
+ new_attributes_hash, rest, last_line = parse_new_attributes(rest)
524
+ attributes_hashes << [:new, new_attributes_hash]
525
+ when ?[
526
+ break if object_ref
527
+ object_ref, rest = balance(rest, ?[, ?])
528
+ else; break
529
+ end
530
+ end
531
+
511
532
  if rest
512
- object_ref, rest = balance(rest, ?[, ?]) if rest[0] == ?[
513
- attributes_hash, rest = parse_attributes(rest) if rest[0] == ?{ && attributes_hash.nil?
514
533
  nuke_whitespace, action, value = rest.scan(/(<>|><|[><])?([=\/\~&!])?(.*)?/)[0]
515
534
  nuke_whitespace ||= ''
516
535
  nuke_outer_whitespace = nuke_whitespace.include? '>'
517
536
  nuke_inner_whitespace = nuke_whitespace.include? '<'
518
537
  end
538
+
519
539
  value = value.to_s.strip
520
- [tag_name, attributes, attributes_hash, object_ref, nuke_outer_whitespace,
521
- nuke_inner_whitespace, action, value]
540
+ [tag_name, attributes, attributes_hashes, object_ref, nuke_outer_whitespace,
541
+ nuke_inner_whitespace, action, value, last_line || @index]
542
+ end
543
+
544
+ def parse_old_attributes(line)
545
+ line = line.dup
546
+ last_line = @index
547
+
548
+ begin
549
+ attributes_hash, rest = balance(line, ?{, ?})
550
+ rescue SyntaxError => e
551
+ if line.strip[-1] == ?, && e.message == "Unbalanced brackets."
552
+ line << "\n" << @next_line.text
553
+ last_line += 1
554
+ next_line
555
+ retry
556
+ end
557
+
558
+ raise e
559
+ end
560
+
561
+ attributes_hash = attributes_hash[1...-1] if attributes_hash
562
+ return attributes_hash, rest, last_line
522
563
  end
523
564
 
524
- def parse_attributes(line)
565
+ def parse_new_attributes(line)
566
+ line = line.dup
525
567
  scanner = StringScanner.new(line)
526
- attributes_hash, rest = balance(scanner, ?{, ?})
527
- attributes_hash = attributes_hash[1...-1] if attributes_hash
528
- return attributes_hash, rest
568
+ last_line = @index
569
+ attributes = {}
570
+
571
+ scanner.scan(/\(\s*/)
572
+ until (name, value = parse_new_attribute(scanner)).first.nil?
573
+ if name == false
574
+ text = (Haml::Shared.balance(line, ?(, ?)) || [line]).first
575
+ raise Haml::SyntaxError.new("Invalid attribute list: #{text.inspect}.", last_line - 1)
576
+ end
577
+ attributes[name] = value
578
+ scanner.scan(/\s*/)
579
+
580
+ if scanner.eos?
581
+ line << " " << @next_line.text
582
+ last_line += 1
583
+ next_line
584
+ scanner.scan(/\s*/)
585
+ end
586
+ end
587
+
588
+ static_attributes = {}
589
+ dynamic_attributes = "{"
590
+ attributes.each do |name, (type, val)|
591
+ if type == :static
592
+ static_attributes[name] = val
593
+ else
594
+ dynamic_attributes << name.inspect << " => " << val << ","
595
+ end
596
+ end
597
+ dynamic_attributes << "}"
598
+ dynamic_attributes = nil if dynamic_attributes == "{}"
599
+
600
+ return [static_attributes, dynamic_attributes], scanner.rest, last_line
601
+ end
602
+
603
+ def parse_new_attribute(scanner)
604
+ unless name = scanner.scan(/[-:\w]+/)
605
+ return if scanner.scan(/\)/)
606
+ return false
607
+ end
608
+
609
+ scanner.scan(/\s*/)
610
+ return name, [:static, true] unless scanner.scan(/=/) #/end
611
+
612
+ scanner.scan(/\s*/)
613
+ unless quote = scanner.scan(/["']/)
614
+ return false unless var = scanner.scan(/(@@?|\$)?\w+/)
615
+ return name, [:dynamic, var]
616
+ end
617
+
618
+ re = /((?:\\.|\#[^{]|[^#{quote}\\#])*)(#{quote}|#\{)/
619
+ content = []
620
+ loop do
621
+ return false unless scanner.scan(re)
622
+ content << [:str, scanner[1].gsub(/\\(.)/, '\1')]
623
+ break if scanner[2] == quote
624
+ content << [:ruby, balance(scanner, ?{, ?}, 1).first[0...-1]]
625
+ end
626
+
627
+ return name, [:static, content.first[1]] if content.size == 1
628
+ return name, [:dynamic,
629
+ '"' + content.map {|(t, v)| t == :str ? v.inspect[1...-1] : "\#{#{v}}"}.join + '"']
529
630
  end
530
631
 
531
632
  # Parses a line that will render as an XHTML tag, and adds the code that will
532
633
  # render that tag to <tt>@precompiled</tt>.
533
634
  def render_tag(line)
534
- tag_name, attributes, attributes_hash, object_ref, nuke_outer_whitespace,
535
- nuke_inner_whitespace, action, value = parse_tag(line)
635
+ tag_name, attributes, attributes_hashes, object_ref, nuke_outer_whitespace,
636
+ nuke_inner_whitespace, action, value, last_line = parse_tag(line)
536
637
 
537
638
  raise SyntaxError.new("Illegal element: classes and ids must have values.") if attributes =~ /[\.#](\.|#|\z)/
538
639
 
@@ -552,7 +653,20 @@ END
552
653
  when '&', '!'
553
654
  if value[0] == ?=
554
655
  parse = true
555
- value = (value[1] == ?= ? unescape_interpolation(value[2..-1].strip) : value[1..-1].strip)
656
+ value =
657
+ if value[1] == ?=
658
+ unescape_interpolation(value[2..-1].strip)
659
+ else
660
+ value[1..-1].strip
661
+ end
662
+ elsif contains_interpolation?(value)
663
+ parse = true
664
+ value = unescape_interpolation(value)
665
+ end
666
+ else
667
+ if contains_interpolation?(value)
668
+ parse = true
669
+ value = unescape_interpolation(value)
556
670
  end
557
671
  end
558
672
 
@@ -565,25 +679,32 @@ END
565
679
 
566
680
  object_ref = "nil" if object_ref.nil? || @options[:suppress_eval]
567
681
 
568
- static_attributes = parse_static_hash(attributes_hash) # Try pre-compiling a static attributes hash
569
- attributes_hash = nil if static_attributes || @options[:suppress_eval]
570
682
  attributes = parse_class_and_id(attributes)
571
- Buffer.merge_attrs(attributes, static_attributes) if static_attributes
683
+ attributes_hashes.map! do |syntax, attributes_hash|
684
+ if syntax == :old
685
+ static_attributes = parse_static_hash(attributes_hash)
686
+ attributes_hash = nil if static_attributes || @options[:suppress_eval]
687
+ else
688
+ static_attributes, attributes_hash = attributes_hash
689
+ end
690
+ Buffer.merge_attrs(attributes, static_attributes) if static_attributes
691
+ attributes_hash
692
+ end.compact!
572
693
 
573
- raise SyntaxError.new("Illegal nesting: nesting within a self-closing tag is illegal.", @next_line.index) if @block_opened && self_closing
574
- raise SyntaxError.new("Illegal nesting: content can't be both given on the same line as %#{tag_name} and nested within it.", @next_line.index) if @block_opened && !value.empty?
575
- raise SyntaxError.new("There's no Ruby code for #{action} to evaluate.") if parse && value.empty?
576
- raise SyntaxError.new("Self-closing tags can't have content.") if self_closing && !value.empty?
694
+ raise SyntaxError.new("Illegal nesting: nesting within a self-closing tag is illegal.", @next_line.index) if block_opened? && self_closing
695
+ raise SyntaxError.new("Illegal nesting: content can't be both given on the same line as %#{tag_name} and nested within it.", @next_line.index) if block_opened? && !value.empty?
696
+ raise SyntaxError.new("There's no Ruby code for #{action} to evaluate.", last_line - 1) if parse && value.empty?
697
+ raise SyntaxError.new("Self-closing tags can't have content.", last_line - 1) if self_closing && !value.empty?
577
698
 
578
- self_closing ||= !!( !@block_opened && value.empty? && @options[:autoclose].include?(tag_name) )
699
+ self_closing ||= !!( !block_opened? && value.empty? && @options[:autoclose].include?(tag_name) )
579
700
 
580
701
  dont_indent_next_line =
581
- (nuke_outer_whitespace && !@block_opened) ||
582
- (nuke_inner_whitespace && @block_opened)
702
+ (nuke_outer_whitespace && !block_opened?) ||
703
+ (nuke_inner_whitespace && block_opened?)
583
704
 
584
705
  # Check if we can render the tag directly to text and not process it in the buffer
585
- if object_ref == "nil" && attributes_hash.nil? && !preserve_script
586
- tag_closed = !@block_opened && !self_closing && !parse
706
+ if object_ref == "nil" && attributes_hashes.empty? && !preserve_script
707
+ tag_closed = !block_opened? && !self_closing && !parse
587
708
 
588
709
  open_tag = prerender_tag(tag_name, self_closing, attributes)
589
710
  if tag_closed
@@ -601,11 +722,18 @@ END
601
722
  else
602
723
  flush_merged_text
603
724
  content = value.empty? || parse ? 'nil' : value.dump
604
- attributes_hash = ', ' + attributes_hash if attributes_hash
605
- args = [tag_name, self_closing, !@block_opened, preserve_tag, escape_html,
725
+ if attributes_hashes.empty?
726
+ attributes_hashes = ''
727
+ elsif attributes_hashes.size == 1
728
+ attributes_hashes = ", #{attributes_hashes.first}"
729
+ else
730
+ attributes_hashes = ", (#{attributes_hashes.join(").merge(")})"
731
+ end
732
+
733
+ args = [tag_name, self_closing, !block_opened?, preserve_tag, escape_html,
606
734
  attributes, nuke_outer_whitespace, nuke_inner_whitespace
607
735
  ].map { |v| v.inspect }.join(', ')
608
- push_silent "_hamlout.open_tag(#{args}, #{object_ref}, #{content}#{attributes_hash})"
736
+ push_silent "_hamlout.open_tag(#{args}, #{object_ref}, #{content}#{attributes_hashes})"
609
737
  @dont_tab_up_next_text = @dont_indent_next_line = dont_indent_next_line
610
738
  end
611
739
 
@@ -618,9 +746,9 @@ END
618
746
  end
619
747
 
620
748
  if parse
621
- flush_merged_text
622
- push_script(value, preserve_script, true, preserve_tag, escape_html, nuke_inner_whitespace)
623
- @dont_tab_up_next_text = true
749
+ push_script(value, :preserve_script => preserve_script, :in_tag => true,
750
+ :preserve_tag => preserve_tag, :escape_html => escape_html,
751
+ :nuke_inner_whitespace => nuke_inner_whitespace)
624
752
  concat_merged_text("</#{tag_name}>" + (nuke_outer_whitespace ? "" : "\n"))
625
753
  end
626
754
  end
@@ -637,7 +765,7 @@ END
637
765
  line.strip!
638
766
  conditional << ">" if conditional
639
767
 
640
- if @block_opened && !line.empty?
768
+ if block_opened? && !line.empty?
641
769
  raise SyntaxError.new('Illegal nesting: nesting within a tag that already has content is illegal.', @next_line.index)
642
770
  end
643
771
 
@@ -659,7 +787,7 @@ END
659
787
 
660
788
  # Renders an XHTML doctype or XML shebang.
661
789
  def render_doctype(line)
662
- raise SyntaxError.new("Illegal nesting: nesting within a header command is illegal.", @next_line.index) if @block_opened
790
+ raise SyntaxError.new("Illegal nesting: nesting within a header command is illegal.", @next_line.index) if block_opened?
663
791
  doctype = text_for_doctype(line)
664
792
  push_text doctype if doctype
665
793
  end
@@ -706,9 +834,80 @@ END
706
834
  raise Error.new("Filter \"#{name}\" is not defined.") unless filter = Filters.defined[name]
707
835
 
708
836
  push_and_tabulate([:filtered, filter])
709
- @flat_spaces = @template_tabs * 2
837
+ @flat = true
710
838
  @filter_buffer = String.new
711
- @block_opened = false
839
+
840
+ # If we don't know the indentation by now, it'll be set in Line#tabs
841
+ @flat_spaces = @indentation * @template_tabs if @indentation
842
+ end
843
+
844
+ def raw_next_line
845
+ text = @template.shift
846
+ return unless text
847
+
848
+ index = @template_index
849
+ @template_index += 1
850
+
851
+ return text, index
852
+ end
853
+
854
+ def next_line
855
+ text, index = raw_next_line
856
+ return unless text
857
+
858
+ # :eod is a special end-of-document marker
859
+ line =
860
+ if text == :eod
861
+ Line.new '-#', '-#', '-#', index, self, true
862
+ else
863
+ Line.new text.strip, text.lstrip.chomp, text, index, self, false
864
+ end
865
+
866
+ # `flat?' here is a little outdated,
867
+ # so we have to manually check if either the previous or current line
868
+ # closes the flat block,
869
+ # as well as whether a new block is opened
870
+ @line.tabs if @line
871
+ unless (flat? && !closes_flat?(line) && !closes_flat?(@line)) ||
872
+ (@line && @line.text[0] == ?: && line.full =~ %r[^#{@line.full[/^\s+/]}\s])
873
+ if line.text.empty?
874
+ newline
875
+ return next_line
876
+ end
877
+
878
+ handle_multiline(line)
879
+ end
880
+
881
+ @next_line = line
882
+ end
883
+
884
+ def closes_flat?(line)
885
+ line && !line.text.empty? && line.full !~ /^#{@flat_spaces}/
886
+ end
887
+
888
+ def un_next_line(line)
889
+ @template.unshift line
890
+ @template_index -= 1
891
+ end
892
+
893
+ def handle_multiline(line)
894
+ if is_multiline?(line.text)
895
+ line.text.slice!(-1)
896
+ while new_line = raw_next_line.first
897
+ break if new_line == :eod
898
+ newline and next if new_line.strip.empty?
899
+ break unless is_multiline?(new_line.strip)
900
+ line.text << new_line.strip[0...-1]
901
+ newline
902
+ end
903
+ un_next_line new_line
904
+ resolve_newlines
905
+ end
906
+ end
907
+
908
+ # Checks whether or not +line+ is in a multiline sequence.
909
+ def is_multiline?(text)
910
+ text && text.length > 1 && text[-1] == MULTILINE_CHAR_VALUE && text[-2] == ?\s
712
911
  end
713
912
 
714
913
  def contains_interpolation?(str)
@@ -716,48 +915,27 @@ END
716
915
  end
717
916
 
718
917
  def unescape_interpolation(str)
719
- scan = StringScanner.new(str.dump)
720
- str = ''
721
-
722
- while scan.scan(/(.*?)(\\+)\#\{/)
918
+ res = ''
919
+ rest = Haml::Shared.handle_interpolation str.dump do |scan|
723
920
  escapes = (scan[2].size - 1) / 2
724
- str << scan.matched[0...-3 - escapes]
921
+ res << scan.matched[0...-3 - escapes]
725
922
  if escapes % 2 == 1
726
- str << '#{'
923
+ res << '#{'
727
924
  else
728
- # Use eval to get rid of string escapes
729
- str << '#{' + eval('"' + balance(scan, ?{, ?}, 1)[0][0...-1] + '"') + "}"
925
+ res << '#{' + eval('"' + balance(scan, ?{, ?}, 1)[0][0...-1] + '"') + "}"# Use eval to get rid of string escapes
730
926
  end
731
927
  end
732
-
733
- str + scan.rest
928
+ res + rest
734
929
  end
735
930
 
736
- def balance(scanner, start, finish, count = 0)
737
- str = ''
738
- scanner = StringScanner.new(scanner) unless scanner.is_a? StringScanner
739
- regexp = Regexp.new("(.*?)[\\#{start.chr}\\#{finish.chr}]")
740
- while scanner.scan(regexp)
741
- str << scanner.matched
742
- count += 1 if scanner.matched[-1] == start
743
- count -= 1 if scanner.matched[-1] == finish
744
- return [str.strip, scanner.rest] if count == 0
745
- end
746
-
931
+ def balance(*args)
932
+ res = Haml::Shared.balance(*args)
933
+ return res if res
747
934
  raise SyntaxError.new("Unbalanced brackets.")
748
935
  end
749
936
 
750
- # Counts the tabulation of a line.
751
- def count_soft_tabs(line)
752
- spaces = line.index(/([^ ]|$)/)
753
- if line[spaces] == ?\t
754
- return 0, 0 if line.strip.empty?
755
- raise SyntaxError.new(<<END.strip, @next_line.index)
756
- A tab character was used for indentation. Haml must be indented using two spaces.
757
- Are you sure you have soft tabs enabled in your editor?
758
- END
759
- end
760
- [spaces, spaces/2]
937
+ def block_opened?
938
+ !flat? && @next_line.tabs > @line.tabs
761
939
  end
762
940
 
763
941
  # Pushes value onto <tt>@to_close_stack</tt> and increases
@@ -768,7 +946,7 @@ END
768
946
  end
769
947
 
770
948
  def flat?
771
- @flat_spaces != -1
949
+ @flat
772
950
  end
773
951
 
774
952
  def newline
@@ -789,11 +967,24 @@ END
789
967
  # Get rid of and whitespace at the end of the buffer
790
968
  # or the merged text
791
969
  def rstrip_buffer!
792
- unless @merged_text.empty?
793
- @merged_text.rstrip!
794
- else
970
+ if @to_merge.empty?
795
971
  push_silent("_hamlout.rstrip!", false)
796
972
  @dont_tab_up_next_text = true
973
+ return
974
+ end
975
+
976
+ last = @to_merge.last
977
+ case last.first
978
+ when :text
979
+ last[1].rstrip!
980
+ if last[1].empty?
981
+ @to_merge.pop
982
+ rstrip_buffer!
983
+ end
984
+ when :script
985
+ last[1].gsub!(/\(haml_temp, (.*?)\);$/, '(haml_temp.rstrip, \1);')
986
+ else
987
+ raise SyntaxError.new("[HAML BUG] Undefined entry in Haml::Precompiler@to_merge.")
797
988
  end
798
989
  end
799
990
  end