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