haml 1.8.0 → 2.0.3

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. data/FAQ +138 -0
  2. data/MIT-LICENSE +1 -1
  3. data/{README → README.rdoc} +66 -3
  4. data/REVISION +1 -0
  5. data/Rakefile +115 -147
  6. data/VERSION +1 -1
  7. data/bin/css2sass +0 -0
  8. data/bin/haml +2 -1
  9. data/bin/html2haml +0 -0
  10. data/bin/sass +0 -0
  11. data/init.rb +6 -1
  12. data/lib/haml/buffer.rb +122 -64
  13. data/lib/haml/engine.rb +77 -46
  14. data/lib/haml/error.rb +15 -6
  15. data/lib/haml/exec.rb +61 -10
  16. data/lib/haml/filters.rb +229 -74
  17. data/lib/haml/helpers/action_view_extensions.rb +1 -1
  18. data/lib/haml/helpers/action_view_mods.rb +109 -24
  19. data/lib/haml/helpers.rb +137 -76
  20. data/lib/haml/html.rb +8 -8
  21. data/lib/haml/precompiler.rb +280 -153
  22. data/lib/haml/template/patch.rb +10 -3
  23. data/lib/haml/template/plugin.rb +61 -10
  24. data/lib/haml/template.rb +14 -9
  25. data/lib/haml.rb +483 -214
  26. data/lib/sass/constant/color.rb +13 -13
  27. data/lib/sass/constant/literal.rb +8 -7
  28. data/lib/sass/constant/nil.rb +9 -0
  29. data/lib/sass/constant/number.rb +10 -10
  30. data/lib/sass/constant/operation.rb +4 -4
  31. data/lib/sass/constant/string.rb +3 -3
  32. data/lib/sass/constant.rb +46 -77
  33. data/lib/sass/css.rb +130 -56
  34. data/lib/sass/engine.rb +131 -43
  35. data/lib/sass/plugin/merb.rb +48 -12
  36. data/lib/sass/plugin/rails.rb +10 -4
  37. data/lib/sass/plugin.rb +33 -10
  38. data/lib/sass/tree/attr_node.rb +5 -5
  39. data/lib/sass/tree/directive_node.rb +2 -7
  40. data/lib/sass/tree/node.rb +1 -12
  41. data/lib/sass/tree/rule_node.rb +39 -31
  42. data/lib/sass/tree/value_node.rb +1 -1
  43. data/lib/sass.rb +194 -19
  44. data/rails/init.rb +1 -0
  45. data/test/benchmark.rb +67 -80
  46. data/test/haml/engine_test.rb +368 -152
  47. data/test/haml/helper_test.rb +68 -16
  48. data/test/haml/html2haml_test.rb +3 -4
  49. data/test/haml/results/content_for_layout.xhtml +1 -2
  50. data/test/haml/results/eval_suppressed.xhtml +2 -4
  51. data/test/haml/results/filters.xhtml +38 -30
  52. data/test/haml/results/helpers.xhtml +4 -8
  53. data/test/haml/results/just_stuff.xhtml +8 -7
  54. data/test/haml/results/nuke_inner_whitespace.xhtml +40 -0
  55. data/test/haml/results/nuke_outer_whitespace.xhtml +148 -0
  56. data/test/haml/results/original_engine.xhtml +3 -7
  57. data/test/haml/results/partials.xhtml +1 -0
  58. data/test/haml/results/tag_parsing.xhtml +1 -6
  59. data/test/haml/results/very_basic.xhtml +2 -4
  60. data/test/haml/results/whitespace_handling.xhtml +13 -21
  61. data/test/haml/template_test.rb +42 -57
  62. data/test/haml/templates/_partial.haml +1 -0
  63. data/test/haml/templates/filters.haml +39 -21
  64. data/test/haml/templates/helpers.haml +10 -10
  65. data/test/haml/templates/just_stuff.haml +8 -3
  66. data/test/haml/templates/nuke_inner_whitespace.haml +32 -0
  67. data/test/haml/templates/nuke_outer_whitespace.haml +144 -0
  68. data/test/haml/templates/partials.haml +1 -1
  69. data/test/haml/templates/tag_parsing.haml +0 -3
  70. data/test/haml/templates/whitespace_handling.haml +10 -10
  71. data/test/sass/engine_test.rb +97 -39
  72. data/test/sass/plugin_test.rb +4 -7
  73. data/test/sass/results/constants.css +2 -0
  74. data/test/sass/results/import.css +2 -2
  75. data/test/sass/results/mixins.css +95 -0
  76. data/test/sass/results/multiline.css +24 -0
  77. data/test/sass/templates/constants.sass +3 -0
  78. data/test/sass/templates/import.sass +4 -1
  79. data/test/sass/templates/importee.sass +4 -0
  80. data/test/sass/templates/mixins.sass +76 -0
  81. data/test/sass/templates/multiline.sass +20 -0
  82. data/test/test_helper.rb +18 -0
  83. metadata +70 -53
  84. data/lib/haml/helpers/action_view_mods.rb.rej +0 -30
  85. data/lib/haml/util.rb +0 -18
  86. data/lib/sass/constant.rb.rej +0 -42
  87. data/test/haml/runner.rb +0 -16
  88. data/test/profile.rb +0 -65
  89. data/test/sass/engine_test.rb.rej +0 -18
@@ -14,12 +14,15 @@ module Haml
14
14
  # Designates an XHTML/XML comment.
15
15
  COMMENT = ?/
16
16
 
17
- # Designates an XHTML doctype.
17
+ # Designates an XHTML doctype or script that is never HTML-escaped.
18
18
  DOCTYPE = ?!
19
19
 
20
20
  # Designates script, the result of which is output.
21
21
  SCRIPT = ?=
22
22
 
23
+ # Designates script that is always HTML-escaped.
24
+ SANITIZE = ?&
25
+
23
26
  # Designates script, the result of which is flattened and output.
24
27
  FLAT_SCRIPT = ?~
25
28
 
@@ -47,6 +50,7 @@ module Haml
47
50
  COMMENT,
48
51
  DOCTYPE,
49
52
  SCRIPT,
53
+ SANITIZE,
50
54
  FLAT_SCRIPT,
51
55
  SILENT_SCRIPT,
52
56
  ESCAPE,
@@ -75,15 +79,9 @@ module Haml
75
79
  # is a member of this array.
76
80
  MID_BLOCK_KEYWORDS = ['else', 'elsif', 'rescue', 'ensure', 'when']
77
81
 
78
- # The Regex that matches an HTML comment command.
79
- COMMENT_REGEX = /\/(\[[\w\s\.]*\])?(.*)/
80
-
81
82
  # The Regex that matches a Doctype command.
82
83
  DOCTYPE_REGEX = /(\d\.\d)?[\s]*([a-z]*)/i
83
84
 
84
- # The Regex that matches an HTML tag command.
85
- TAG_REGEX = /[%]([-:\w]+)([-\w\.\#]*)(\{.*\})?(\[.*\])?([=\/\~]?)?(.*)?/
86
-
87
85
  # The Regex that matches a literal string or symbol value
88
86
  LITERAL_VALUE_REGEX = /^\s*(:(\w*)|(('|")([^\\\#'"]*?)\4))\s*$/
89
87
 
@@ -93,15 +91,12 @@ module Haml
93
91
  def precompiled_with_ambles(local_names)
94
92
  preamble = <<END.gsub("\n", ";")
95
93
  extend Haml::Helpers
96
- @haml_stack ||= Array.new
97
- @haml_stack.push(Haml::Buffer.new(#{options_for_buffer.inspect}))
98
- @haml_is_haml = true
99
- _hamlout = @haml_stack[-1]
94
+ _hamlout = @haml_buffer = Haml::Buffer.new(@haml_buffer, #{options_for_buffer.inspect})
100
95
  _erbout = _hamlout.buffer
101
96
  END
102
97
  postamble = <<END.gsub("\n", ";")
103
- @haml_is_haml = false
104
- @haml_stack.pop.buffer
98
+ @haml_buffer = @haml_buffer.upper
99
+ _erbout
105
100
  END
106
101
  preamble + locals_code(local_names) + @precompiled + postamble
107
102
  end
@@ -114,42 +109,32 @@ END
114
109
  end.join(';') + ';'
115
110
  end
116
111
 
117
- Line = Struct.new("Line", :text, :unstripped, :index, :spaces, :tabs)
112
+ Line = Struct.new(:text, :unstripped, :index, :spaces, :tabs)
118
113
 
119
114
  def precompile
120
- @precompiled = ''
121
- @merged_text = ''
122
- @tab_change = 0
123
-
115
+ @haml_comment = @dont_indent_next_line = @dont_tab_up_next_text = false
116
+ @indentation = nil
124
117
  old_line = Line.new
125
- (@template + "\n-#").split("\n").each_with_index do |text, index|
126
- line = Line.new text.strip, text.lstrip.chomp, index
118
+ @template.split(/\r\n|\r|\n/).each_with_index do |text, index|
119
+ @next_line = line = Line.new(text.strip, text.lstrip.chomp, index)
127
120
  line.spaces, line.tabs = count_soft_tabs(text)
128
121
 
129
- if line.text.empty?
130
- process_indent(old_line) unless !flat? || old_line.text.empty?
131
-
132
- unless flat?
133
- newline
134
- next
135
- end
136
-
137
- push_flat(old_line)
138
- old_line.text, old_line.unstripped, old_line.spaces = '', '', 0
139
- newline
140
- next
141
- end
142
-
143
122
  suppress_render = handle_multiline(old_line) unless flat?
144
123
 
145
124
  if old_line.text.nil? || suppress_render
146
125
  old_line = line
126
+ resolve_newlines
147
127
  newline
148
128
  next
149
129
  end
150
130
 
151
131
  process_indent(old_line) unless old_line.text.empty?
152
132
 
133
+ if line.text.empty? && !flat?
134
+ newline
135
+ next
136
+ end
137
+
153
138
  if flat?
154
139
  push_flat(old_line)
155
140
  old_line = line
@@ -158,15 +143,20 @@ END
158
143
  end
159
144
 
160
145
  if old_line.spaces != old_line.tabs * 2
161
- raise SyntaxError.new("Illegal Indentation: Only two space characters are allowed as tabulation.")
146
+ raise SyntaxError.new(<<END.strip, old_line.index)
147
+ #{old_line.spaces} space#{old_line.spaces == 1 ? ' was' : 's were'} used for indentation. Haml must be indented using two spaces.
148
+ END
162
149
  end
163
150
 
164
151
  unless old_line.text.empty? || @haml_comment
165
152
  process_line(old_line.text, old_line.index, line.tabs > old_line.tabs && !line.text.empty?)
166
153
  end
154
+ resolve_newlines
167
155
 
168
156
  if !flat? && line.tabs - old_line.tabs > 1
169
- raise SyntaxError.new("Illegal Indentation: Indenting more than once per line is illegal.")
157
+ raise SyntaxError.new(<<END.strip, line.index)
158
+ #{line.spaces} spaces were used for indentation. Haml must be indented using two spaces.
159
+ END
170
160
  end
171
161
  old_line = line
172
162
  newline
@@ -176,7 +166,7 @@ END
176
166
  close until @to_close_stack.empty?
177
167
  flush_merged_text
178
168
  end
179
-
169
+
180
170
  # Processes and deals with lowering indentation.
181
171
  def process_indent(line)
182
172
  return unless line.tabs <= @template_tabs && @template_tabs > 0
@@ -196,28 +186,43 @@ END
196
186
  case text[0]
197
187
  when DIV_CLASS, DIV_ID; render_div(text)
198
188
  when ELEMENT; render_tag(text)
199
- when COMMENT; render_comment(text)
189
+ when COMMENT; render_comment(text[1..-1].strip)
190
+ when SANITIZE
191
+ return push_script(unescape_interpolation(text[3..-1].strip), false, false, false, true) if text[1..2] == "=="
192
+ return push_script(text[2..-1].strip, false, false, false, true) if text[1] == SCRIPT
193
+ push_plain text
200
194
  when SCRIPT
201
195
  return push_script(unescape_interpolation(text[2..-1].strip), false) if text[1] == SCRIPT
196
+ return push_script(text[1..-1], false, false, false, true) if options[:escape_html]
202
197
  push_script(text[1..-1], false)
203
198
  when FLAT_SCRIPT; push_flat_script(text[1..-1])
204
199
  when SILENT_SCRIPT
205
200
  return start_haml_comment if text[1] == SILENT_COMMENT
206
201
 
202
+ raise SyntaxError.new(<<END.rstrip, index) if text[1..-1].strip == "end"
203
+ You don't need to use "- end" in Haml. Use indentation instead:
204
+ - if foo?
205
+ %strong Foo!
206
+ - else
207
+ Not foo.
208
+ END
209
+
207
210
  push_silent(text[1..-1], true)
208
- newline true
211
+ newline_now
209
212
  if (@block_opened && !mid_block_keyword?(text)) || text[1..-1].split(' ', 2)[0] == "case"
210
213
  push_and_tabulate([:script])
211
214
  end
212
215
  when FILTER; start_filtered(text[1..-1].downcase)
213
216
  when DOCTYPE
214
217
  return render_doctype(text) if text[0...3] == '!!!'
218
+ return push_script(unescape_interpolation(text[3..-1].strip), false) if text[1..2] == "=="
219
+ return push_script(text[2..-1].strip, false) if text[1] == SCRIPT
215
220
  push_plain text
216
221
  when ESCAPE; push_plain text[1..-1]
217
222
  else push_plain text
218
223
  end
219
224
  end
220
-
225
+
221
226
  # Returns whether or not the text is a silent script text with one
222
227
  # of Ruby's mid-block keywords.
223
228
  def mid_block_keyword?(text)
@@ -237,7 +242,7 @@ END
237
242
  @multiline.text << text[0...-1]
238
243
  return true
239
244
  end
240
-
245
+
241
246
  # A multiline string has just been activated, start adding the lines
242
247
  if is_multiline?(text) && (MULTILINE_STARTERS.include? text[0])
243
248
  @multiline = Line.new text[0...-1], nil, line.index, nil, line.tabs
@@ -262,38 +267,47 @@ END
262
267
  # Evaluates <tt>text</tt> in the context of the scope object, but
263
268
  # does not output the result.
264
269
  def push_silent(text, can_suppress = false)
265
- flush_merged_text
270
+ flush_merged_text
266
271
  return if can_suppress && options[:suppress_eval]
267
272
  @precompiled << "#{text};"
268
273
  end
269
274
 
270
275
  # Adds <tt>text</tt> to <tt>@buffer</tt> with appropriate tabulation
271
276
  # without parsing it.
272
- def push_merged_text(text, tab_change = 0, try_one_liner = false)
273
- @merged_text << "#{' ' * @output_tabs}#{text}"
277
+ def push_merged_text(text, tab_change = 0, indent = true)
278
+ @merged_text << (!indent || @dont_indent_next_line || @options[:ugly] ? text : "#{' ' * @output_tabs}#{text}")
279
+ @dont_indent_next_line = false
274
280
  @tab_change += tab_change
275
- @try_one_liner = try_one_liner
276
281
  end
277
-
278
- def push_text(text, tab_change = 0, try_one_liner = false)
279
- push_merged_text("#{text}\n", tab_change, try_one_liner)
282
+
283
+ # Concatenate <tt>text</tt> to <tt>@buffer</tt> without tabulation.
284
+ def concat_merged_text(text)
285
+ @merged_text << text
286
+ end
287
+
288
+ def push_text(text, tab_change = 0)
289
+ push_merged_text("#{text}\n", tab_change)
280
290
  end
281
-
291
+
282
292
  def flush_merged_text
283
293
  return if @merged_text.empty?
284
294
 
285
295
  @precompiled << "_hamlout.push_text(#{@merged_text.dump}"
286
- @precompiled << ", #{@tab_change}" if @tab_change != 0 || @try_one_liner
296
+ @precompiled << ", #{@dont_tab_up_next_text.inspect}" if @dont_tab_up_next_text || @tab_change != 0
297
+ @precompiled << ", #{@tab_change}" if @tab_change != 0
287
298
  @precompiled << ");"
288
299
  @merged_text = ''
300
+ @dont_tab_up_next_text = false
289
301
  @tab_change = 0
290
- @try_one_liner = false
291
- end
302
+ end
292
303
 
293
304
  # Renders a block of text as plain text.
294
305
  # Also checks for an illegally opened block.
295
306
  def push_plain(text)
296
- raise SyntaxError.new("Illegal Nesting: Nesting within plain text is illegal.") if @block_opened
307
+ if @block_opened
308
+ raise SyntaxError.new("Illegal nesting: nesting within plain text is illegal.", @next_line.index)
309
+ end
310
+
297
311
  push_text text
298
312
  end
299
313
 
@@ -307,28 +321,36 @@ END
307
321
  # Causes <tt>text</tt> to be evaluated in the context of
308
322
  # the scope object and the result to be added to <tt>@buffer</tt>.
309
323
  #
310
- # If <tt>flattened</tt> is true, Haml::Helpers#find_and_flatten is run on
324
+ # If <tt>preserve_script</tt> is true, Haml::Helpers#find_and_flatten is run on
311
325
  # the result before it is added to <tt>@buffer</tt>
312
- def push_script(text, flattened, close_tag = nil)
326
+ def push_script(text, preserve_script, in_tag = false, preserve_tag = false,
327
+ escape_html = false, nuke_inner_whitespace = false)
328
+ # Prerender tabulation unless we're in a tag
329
+ push_merged_text '' unless in_tag
330
+
313
331
  flush_merged_text
314
332
  return if options[:suppress_eval]
315
333
 
334
+ raise SyntaxError.new("There's no Ruby code for = to evaluate.") if text.empty?
335
+
316
336
  push_silent "haml_temp = #{text}"
317
- newline true
318
- out = "haml_temp = _hamlout.push_script(haml_temp, #{flattened.inspect}, #{close_tag.inspect});"
337
+ newline_now
338
+ args = [preserve_script, in_tag, preserve_tag,
339
+ escape_html, nuke_inner_whitespace].map { |a| a.inspect }.join(', ')
340
+ out = "haml_temp = _hamlout.push_script(haml_temp, #{args});"
319
341
  if @block_opened
320
342
  push_and_tabulate([:loud, out])
321
343
  else
322
344
  @precompiled << out
323
345
  end
324
346
  end
325
-
347
+
326
348
  # Causes <tt>text</tt> to be evaluated, and Haml::Helpers#find_and_flatten
327
349
  # to be run on it afterwards.
328
350
  def push_flat_script(text)
329
351
  flush_merged_text
330
-
331
- raise SyntaxError.new("Tag has no content.") if text.empty?
352
+
353
+ raise SyntaxError.new("There's no Ruby code for ~ to evaluate.") if text.empty?
332
354
  push_script(text, true)
333
355
  end
334
356
 
@@ -354,10 +376,14 @@ END
354
376
 
355
377
  # Puts a line in <tt>@precompiled</tt> that will add the closing tag of
356
378
  # the most recently opened tag.
357
- def close_tag(tag)
358
- @output_tabs -= 1
379
+ def close_tag(value)
380
+ tag, nuke_outer_whitespace, nuke_inner_whitespace = value
381
+ @output_tabs -= 1 unless nuke_inner_whitespace
359
382
  @template_tabs -= 1
360
- push_text("</#{tag}>", -1)
383
+ rstrip_buffer! if nuke_inner_whitespace
384
+ push_merged_text("</#{tag}>" + (nuke_outer_whitespace ? "" : "\n"),
385
+ nuke_inner_whitespace ? 0 : -1, !nuke_inner_whitespace)
386
+ @dont_indent_next_line = nuke_outer_whitespace
361
387
  end
362
388
 
363
389
  # Closes a Ruby block.
@@ -373,7 +399,7 @@ END
373
399
  close_tag = has_conditional ? "<![endif]-->" : "-->"
374
400
  push_text(close_tag, -1)
375
401
  end
376
-
402
+
377
403
  # Closes a loud Ruby block.
378
404
  def close_loud(command)
379
405
  push_silent 'end', true
@@ -384,14 +410,7 @@ END
384
410
  # Closes a filtered block.
385
411
  def close_filtered(filter)
386
412
  @flat_spaces = -1
387
- filtered = filter.new(@filter_buffer).render
388
-
389
- if filter == Haml::Filters::Preserve
390
- push_silent("_hamlout.buffer << #{filtered.dump} << \"\\n\";")
391
- else
392
- push_text(filtered.rstrip.gsub("\n", "\n#{' ' * @output_tabs}"))
393
- end
394
-
413
+ filter.internal_compile(self, @filter_buffer)
395
414
  @filter_buffer = nil
396
415
  @template_tabs -= 1
397
416
  end
@@ -400,7 +419,7 @@ END
400
419
  @haml_comment = false
401
420
  @template_tabs -= 1
402
421
  end
403
-
422
+
404
423
  # Iterates through the classes and ids supplied through <tt>.</tt>
405
424
  # and <tt>#</tt> syntax, and returns a hash with them as attributes,
406
425
  # that can then be merged with another attributes hash.
@@ -429,8 +448,8 @@ END
429
448
  # $5 holds the value matched by a string
430
449
  $2 || $5
431
450
  end
432
-
433
- def parse_static_hash(text)
451
+
452
+ def parse_static_hash(text)
434
453
  return {} unless text
435
454
 
436
455
  attributes = {}
@@ -449,14 +468,23 @@ END
449
468
  end
450
469
 
451
470
  # This is a class method so it can be accessed from Buffer.
452
- def self.build_attributes(attr_wrapper, attributes = {})
471
+ def self.build_attributes(is_html, attr_wrapper, attributes = {})
453
472
  quote_escape = attr_wrapper == '"' ? "&quot;" : "&apos;"
454
473
  other_quote_char = attr_wrapper == '"' ? "'" : '"'
455
-
474
+
456
475
  result = attributes.collect do |attr, value|
457
476
  next if value.nil?
458
477
 
459
- value = value.to_s
478
+ if value == true
479
+ next " #{attr}" if is_html
480
+ next " #{attr}=#{attr_wrapper}#{attr}#{attr_wrapper}"
481
+ elsif value == false
482
+ next
483
+ end
484
+
485
+ value = Haml::Helpers.preserve(Haml::Helpers.escape_once(value.to_s))
486
+ # We want to decide whether or not to escape quotes
487
+ value.gsub!('&quot;', '"')
460
488
  this_attr_wrapper = attr_wrapper
461
489
  if value.include? attr_wrapper
462
490
  if value.include? other_quote_char
@@ -470,33 +498,71 @@ END
470
498
  result.compact.sort.join
471
499
  end
472
500
 
473
- def prerender_tag(name, atomic, attributes)
474
- "<#{name}#{Precompiler.build_attributes(@options[:attr_wrapper], attributes)}#{atomic ? ' />' : '>'}"
501
+ def prerender_tag(name, self_close, attributes)
502
+ attributes_string = Precompiler.build_attributes(html?, @options[:attr_wrapper], attributes)
503
+ "<#{name}#{attributes_string}#{self_close && xhtml? ? ' /' : ''}>"
504
+ end
505
+
506
+ # Parses a line into tag_name, attributes, attributes_hash, object_ref, action, value
507
+ def parse_tag(line)
508
+ raise SyntaxError.new("Invalid tag: \"#{line}\".") unless match = line.scan(/%([-:\w]+)([-\w\.\#]*)(.*)/)[0]
509
+ tag_name, attributes, rest = match
510
+ attributes_hash, rest = parse_attributes(rest) if rest[0] == ?{
511
+ 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
+ nuke_whitespace, action, value = rest.scan(/(<>|><|[><])?([=\/\~&!])?(.*)?/)[0]
515
+ nuke_whitespace ||= ''
516
+ nuke_outer_whitespace = nuke_whitespace.include? '>'
517
+ nuke_inner_whitespace = nuke_whitespace.include? '<'
518
+ end
519
+ value = value.to_s.strip
520
+ [tag_name, attributes, attributes_hash, object_ref, nuke_outer_whitespace,
521
+ nuke_inner_whitespace, action, value]
522
+ end
523
+
524
+ def parse_attributes(line)
525
+ 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
475
529
  end
476
530
 
477
531
  # Parses a line that will render as an XHTML tag, and adds the code that will
478
532
  # render that tag to <tt>@precompiled</tt>.
479
533
  def render_tag(line)
480
- raise SyntaxError.new("Invalid tag: \"#{line}\"") unless match = line.scan(TAG_REGEX)[0]
481
- tag_name, attributes, attributes_hash, object_ref, action, value = match
482
- value = value.to_s.strip
483
- attributes_hash = attributes_hash[1...-1] if attributes_hash
534
+ tag_name, attributes, attributes_hash, object_ref, nuke_outer_whitespace,
535
+ nuke_inner_whitespace, action, value = parse_tag(line)
484
536
 
485
537
  raise SyntaxError.new("Illegal element: classes and ids must have values.") if attributes =~ /[\.#](\.|#|\z)/
486
538
 
539
+ # Get rid of whitespace outside of the tag if we need to
540
+ rstrip_buffer! if nuke_outer_whitespace
541
+
542
+ preserve_tag = options[:preserve].include?(tag_name)
543
+ nuke_inner_whitespace ||= preserve_tag
544
+ preserve_tag &&= !options[:ugly]
545
+
487
546
  case action
488
- when '/'; atomic = true
489
- when '~'; parse = flattened = true
547
+ when '/'; self_closing = xhtml?
548
+ when '~'; parse = preserve_script = true
490
549
  when '='
491
550
  parse = true
492
551
  value = unescape_interpolation(value[1..-1].strip) if value[0] == ?=
552
+ when '&', '!'
553
+ if value[0] == ?=
554
+ parse = true
555
+ value = (value[1] == ?= ? unescape_interpolation(value[2..-1].strip) : value[1..-1].strip)
556
+ end
493
557
  end
494
-
558
+
495
559
  if parse && @options[:suppress_eval]
496
560
  parse = false
497
561
  value = ''
498
562
  end
499
563
 
564
+ escape_html = (action == '&' || (action != '!' && @options[:escape_html]))
565
+
500
566
  object_ref = "nil" if object_ref.nil? || @options[:suppress_eval]
501
567
 
502
568
  static_attributes = parse_static_hash(attributes_hash) # Try pre-compiling a static attributes hash
@@ -504,41 +570,58 @@ END
504
570
  attributes = parse_class_and_id(attributes)
505
571
  Buffer.merge_attrs(attributes, static_attributes) if static_attributes
506
572
 
507
- raise SyntaxError.new("Illegal Nesting: Nesting within an atomic tag is illegal.") if @block_opened && atomic
508
- raise SyntaxError.new("Illegal Nesting: Nesting within a tag that already has content is illegal.") if @block_opened && !value.empty?
509
- raise SyntaxError.new("Tag has no content.") if parse && value.empty?
510
- raise SyntaxError.new("Atomic tags can't have content.") if atomic && !value.empty?
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?
511
577
 
512
- atomic = true if !@block_opened && value.empty? && @options[:autoclose].include?(tag_name)
513
-
514
- if object_ref == "nil" && attributes_hash.nil? && !flattened && (parse || Buffer.one_liner?(value))
515
- # This means that we can render the tag directly to text and not process it in the buffer
516
- tag_closed = !value.empty? && Buffer.one_liner?(value) && !parse
578
+ self_closing ||= !!( !@block_opened && value.empty? && @options[:autoclose].include?(tag_name) )
517
579
 
518
- open_tag = prerender_tag(tag_name, atomic, attributes)
519
- open_tag << "#{value}</#{tag_name}>" if tag_closed
520
- open_tag << "\n" unless parse
580
+ dont_indent_next_line =
581
+ (nuke_outer_whitespace && !@block_opened) ||
582
+ (nuke_inner_whitespace && @block_opened)
583
+
584
+ # 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
587
+
588
+ open_tag = prerender_tag(tag_name, self_closing, attributes)
589
+ if tag_closed
590
+ open_tag << "#{value}</#{tag_name}>"
591
+ open_tag << "\n" unless nuke_outer_whitespace
592
+ else
593
+ open_tag << "\n" unless parse || nuke_inner_whitespace || (self_closing && nuke_outer_whitespace)
594
+ end
595
+
596
+ push_merged_text(open_tag, tag_closed || self_closing || nuke_inner_whitespace ? 0 : 1,
597
+ !nuke_outer_whitespace)
521
598
 
522
- push_merged_text(open_tag, tag_closed || atomic ? 0 : 1, parse)
599
+ @dont_indent_next_line = dont_indent_next_line
523
600
  return if tag_closed
524
601
  else
525
602
  flush_merged_text
526
603
  content = value.empty? || parse ? 'nil' : value.dump
527
604
  attributes_hash = ', ' + attributes_hash if attributes_hash
528
- push_silent "_hamlout.open_tag(#{tag_name.inspect}, #{atomic.inspect}, #{(!value.empty?).inspect}, #{attributes.inspect}, #{object_ref}, #{content}#{attributes_hash})"
605
+ args = [tag_name, self_closing, !@block_opened, preserve_tag, escape_html,
606
+ attributes, nuke_outer_whitespace, nuke_inner_whitespace
607
+ ].map { |v| v.inspect }.join(', ')
608
+ push_silent "_hamlout.open_tag(#{args}, #{object_ref}, #{content}#{attributes_hash})"
609
+ @dont_tab_up_next_text = @dont_indent_next_line = dont_indent_next_line
529
610
  end
530
-
531
- return if atomic
611
+
612
+ return if self_closing
532
613
 
533
614
  if value.empty?
534
- push_and_tabulate([:element, tag_name])
535
- @output_tabs += 1
615
+ push_and_tabulate([:element, [tag_name, nuke_outer_whitespace, nuke_inner_whitespace]])
616
+ @output_tabs += 1 unless nuke_inner_whitespace
536
617
  return
537
618
  end
538
-
619
+
539
620
  if parse
540
621
  flush_merged_text
541
- push_script(value, flattened, tag_name)
622
+ push_script(value, preserve_script, true, preserve_tag, escape_html, nuke_inner_whitespace)
623
+ @dont_tab_up_next_text = true
624
+ concat_merged_text("</#{tag_name}>" + (nuke_outer_whitespace ? "" : "\n"))
542
625
  end
543
626
  end
544
627
 
@@ -550,92 +633,113 @@ END
550
633
 
551
634
  # Renders an XHTML comment.
552
635
  def render_comment(line)
553
- conditional, content = line.scan(COMMENT_REGEX)[0]
554
- content.strip!
636
+ conditional, line = balance(line, ?[, ?]) if line[0] == ?[
637
+ line.strip!
555
638
  conditional << ">" if conditional
556
-
557
- if @block_opened && !content.empty?
558
- raise SyntaxError.new('Illegal Nesting: Nesting within a tag that already has content is illegal.')
639
+
640
+ if @block_opened && !line.empty?
641
+ raise SyntaxError.new('Illegal nesting: nesting within a tag that already has content is illegal.', @next_line.index)
559
642
  end
560
643
 
561
644
  open = "<!--#{conditional} "
562
-
645
+
563
646
  # Render it statically if possible
564
- if !content.empty? && Buffer.one_liner?(content)
565
- return push_text("#{open}#{content} #{conditional ? "<![endif]-->" : "-->"}")
647
+ unless line.empty?
648
+ return push_text("#{open}#{line} #{conditional ? "<![endif]-->" : "-->"}")
566
649
  end
567
650
 
568
651
  push_text(open, 1)
569
652
  @output_tabs += 1
570
653
  push_and_tabulate([:comment, !conditional.nil?])
571
- unless content.empty?
572
- push_text(content)
654
+ unless line.empty?
655
+ push_text(line)
573
656
  close
574
657
  end
575
658
  end
576
-
659
+
577
660
  # Renders an XHTML doctype or XML shebang.
578
661
  def render_doctype(line)
579
- raise SyntaxError.new("Illegal Nesting: Nesting within a header command is illegal.") if @block_opened
580
- push_text text_for_doctype(line)
662
+ raise SyntaxError.new("Illegal nesting: nesting within a header command is illegal.", @next_line.index) if @block_opened
663
+ doctype = text_for_doctype(line)
664
+ push_text doctype if doctype
581
665
  end
582
666
 
583
667
  def text_for_doctype(text)
584
668
  text = text[3..-1].lstrip.downcase
585
- if text[0...3] == "xml"
669
+ if text.index("xml") == 0
670
+ return nil if html?
586
671
  wrapper = @options[:attr_wrapper]
587
672
  return "<?xml version=#{wrapper}1.0#{wrapper} encoding=#{wrapper}#{text.split(' ')[1] || "utf-8"}#{wrapper} ?>"
588
673
  end
589
674
 
590
- version, type = text.scan(DOCTYPE_REGEX)[0]
591
- if version == "1.1"
592
- return '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">'
593
- end
675
+ if html5?
676
+ '<!DOCTYPE html>'
677
+ else
678
+ version, type = text.scan(DOCTYPE_REGEX)[0]
679
+
680
+ if xhtml?
681
+ if version == "1.1"
682
+ '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">'
683
+ else
684
+ case type
685
+ when "strict"; '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">'
686
+ when "frameset"; '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">'
687
+ else '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">'
688
+ end
689
+ end
594
690
 
595
- case type
596
- when "strict"; return '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">'
597
- when "frameset"; return '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">'
598
- else return '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">'
691
+ elsif html4?
692
+ case type
693
+ when "strict"; '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">'
694
+ when "frameset"; '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Frameset//EN" "http://www.w3.org/TR/html4/frameset.dtd">'
695
+ else '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">'
696
+ end
697
+ end
599
698
  end
600
699
  end
601
700
 
602
701
  # Starts a filtered block.
603
702
  def start_filtered(name)
604
- raise SyntaxError.new('Filters must have nested text.') unless @block_opened
605
-
606
- unless filter = options[:filters][name]
607
- if filter == 'redcloth' || filter == 'markdown' || filter == 'textile'
608
- raise HamlError.new("You must have the RedCloth gem installed to use \"#{name}\" filter")
609
- end
610
- raise HamlError.new("\"#{name}\" filter is not defined!")
611
- end
703
+ raise Error.new("Invalid filter name \":#{name}\".") unless name =~ /^\w+$/
704
+ raise Error.new("Filter \"#{name}\" is not defined.") unless filter = Filters.defined[name]
612
705
 
613
706
  push_and_tabulate([:filtered, filter])
614
707
  @flat_spaces = @template_tabs * 2
615
708
  @filter_buffer = String.new
709
+ @block_opened = false
710
+ end
711
+
712
+ def contains_interpolation?(str)
713
+ str.include?('#{')
616
714
  end
617
715
 
618
716
  def unescape_interpolation(str)
619
717
  scan = StringScanner.new(str.dump)
620
718
  str = ''
621
719
 
622
- while scan.scan(/(.*?)\\\#\{/)
623
- str << scan.matched[0...-3]
624
- str << eval("\"\\\#{#{balance_brackets(scan)}}\"")
720
+ while scan.scan(/(.*?)(\\+)\#\{/)
721
+ escapes = (scan[2].size - 1) / 2
722
+ str << scan.matched[0...-3 - escapes]
723
+ if escapes % 2 == 1
724
+ str << '#{'
725
+ else
726
+ # Use eval to get rid of string escapes
727
+ str << '#{' + eval('"' + balance(scan, ?{, ?}, 1)[0][0...-1] + '"') + "}"
728
+ end
625
729
  end
626
730
 
627
731
  str + scan.rest
628
732
  end
629
733
 
630
- def balance_brackets(scanner)
734
+ def balance(scanner, start, finish, count = 0)
631
735
  str = ''
632
- count = 1
633
-
634
- while scanner.scan(/(.*?)[\{\}]/)
736
+ scanner = StringScanner.new(scanner) unless scanner.is_a? StringScanner
737
+ regexp = Regexp.new("(.*?)[\\#{start.chr}\\#{finish.chr}]")
738
+ while scanner.scan(regexp)
635
739
  str << scanner.matched
636
- count += 1 if scanner.matched[-1] == ?{
637
- count -= 1 if scanner.matched[-1] == ?}
638
- return str[0...-1] if count == 0
740
+ count += 1 if scanner.matched[-1] == start
741
+ count -= 1 if scanner.matched[-1] == finish
742
+ return [str.strip, scanner.rest] if count == 0
639
743
  end
640
744
 
641
745
  raise SyntaxError.new("Unbalanced brackets.")
@@ -645,12 +749,15 @@ END
645
749
  def count_soft_tabs(line)
646
750
  spaces = line.index(/([^ ]|$)/)
647
751
  if line[spaces] == ?\t
648
- return nil if line.strip.empty?
649
- raise SyntaxError.new("Illegal Indentation: Only two space characters are allowed as tabulation.")
752
+ return 0, 0 if line.strip.empty?
753
+ raise SyntaxError.new(<<END.strip, @next_line.index)
754
+ A tab character was used for indentation. Haml must be indented using two spaces.
755
+ Are you sure you have soft tabs enabled in your editor?
756
+ END
650
757
  end
651
758
  [spaces, spaces/2]
652
759
  end
653
-
760
+
654
761
  # Pushes value onto <tt>@to_close_stack</tt> and increases
655
762
  # <tt>@template_tabs</tt>.
656
763
  def push_and_tabulate(value)
@@ -662,10 +769,30 @@ END
662
769
  @flat_spaces != -1
663
770
  end
664
771
 
665
- def newline(skip_next = false)
666
- return @skip_next_newline = false if @skip_next_newline
667
- @skip_next_newline = true if skip_next
772
+ def newline
773
+ @newlines += 1
774
+ end
775
+
776
+ def newline_now
668
777
  @precompiled << "\n"
778
+ @newlines -= 1
779
+ end
780
+
781
+ def resolve_newlines
782
+ return unless @newlines > 0
783
+ @precompiled << "\n" * @newlines
784
+ @newlines = 0
785
+ end
786
+
787
+ # Get rid of and whitespace at the end of the buffer
788
+ # or the merged text
789
+ def rstrip_buffer!
790
+ unless @merged_text.empty?
791
+ @merged_text.rstrip!
792
+ else
793
+ push_silent("_erbout.rstrip!", false)
794
+ @dont_tab_up_next_text = true
795
+ end
669
796
  end
670
797
  end
671
798
  end