haml 1.8.2 → 2.0.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 (77) 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.rb +464 -201
  12. data/lib/haml/buffer.rb +117 -63
  13. data/lib/haml/engine.rb +63 -44
  14. data/lib/haml/error.rb +16 -6
  15. data/lib/haml/exec.rb +37 -7
  16. data/lib/haml/filters.rb +213 -68
  17. data/lib/haml/helpers.rb +95 -60
  18. data/lib/haml/helpers/action_view_extensions.rb +1 -1
  19. data/lib/haml/helpers/action_view_mods.rb +54 -6
  20. data/lib/haml/html.rb +6 -6
  21. data/lib/haml/precompiler.rb +254 -133
  22. data/lib/haml/template.rb +3 -6
  23. data/lib/haml/template/patch.rb +9 -2
  24. data/lib/haml/template/plugin.rb +52 -23
  25. data/lib/sass.rb +157 -12
  26. data/lib/sass/constant.rb +22 -22
  27. data/lib/sass/constant/color.rb +13 -13
  28. data/lib/sass/constant/literal.rb +7 -7
  29. data/lib/sass/constant/number.rb +9 -9
  30. data/lib/sass/constant/operation.rb +4 -4
  31. data/lib/sass/constant/string.rb +3 -3
  32. data/lib/sass/css.rb +104 -31
  33. data/lib/sass/engine.rb +120 -39
  34. data/lib/sass/error.rb +1 -1
  35. data/lib/sass/plugin.rb +14 -3
  36. data/lib/sass/plugin/merb.rb +6 -2
  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/test/benchmark.rb +67 -80
  43. data/test/haml/engine_test.rb +284 -84
  44. data/test/haml/helper_test.rb +51 -15
  45. data/test/haml/results/content_for_layout.xhtml +1 -2
  46. data/test/haml/results/eval_suppressed.xhtml +2 -4
  47. data/test/haml/results/filters.xhtml +44 -15
  48. data/test/haml/results/helpers.xhtml +2 -3
  49. data/test/haml/results/just_stuff.xhtml +2 -6
  50. data/test/haml/results/nuke_inner_whitespace.xhtml +34 -0
  51. data/test/haml/results/nuke_outer_whitespace.xhtml +148 -0
  52. data/test/haml/results/original_engine.xhtml +3 -7
  53. data/test/haml/results/partials.xhtml +1 -0
  54. data/test/haml/results/tag_parsing.xhtml +1 -6
  55. data/test/haml/results/very_basic.xhtml +2 -4
  56. data/test/haml/results/whitespace_handling.xhtml +13 -21
  57. data/test/haml/template_test.rb +8 -15
  58. data/test/haml/templates/_partial.haml +1 -0
  59. data/test/haml/templates/filters.haml +48 -7
  60. data/test/haml/templates/just_stuff.haml +1 -2
  61. data/test/haml/templates/nuke_inner_whitespace.haml +26 -0
  62. data/test/haml/templates/nuke_outer_whitespace.haml +144 -0
  63. data/test/haml/templates/tag_parsing.haml +0 -3
  64. data/test/haml/test_helper.rb +15 -0
  65. data/test/sass/engine_test.rb +80 -34
  66. data/test/sass/plugin_test.rb +1 -1
  67. data/test/sass/results/import.css +2 -2
  68. data/test/sass/results/mixins.css +95 -0
  69. data/test/sass/results/multiline.css +24 -0
  70. data/test/sass/templates/import.sass +4 -1
  71. data/test/sass/templates/importee.sass +4 -0
  72. data/test/sass/templates/mixins.sass +76 -0
  73. data/test/sass/templates/multiline.sass +20 -0
  74. metadata +65 -51
  75. data/lib/haml/util.rb +0 -18
  76. data/test/haml/runner.rb +0 -16
  77. data/test/profile.rb +0 -65
@@ -34,25 +34,44 @@ module Haml
34
34
  # include Haml::Helpers
35
35
  # end
36
36
  # context.init_haml_helpers
37
- # context.open :p, "Stuff"
38
- #
37
+ # context.haml_tag :p, "Stuff"
38
+ #
39
39
  def init_haml_helpers
40
- @haml_is_haml = true
41
- @haml_stack = [Haml::Buffer.new]
40
+ @haml_buffer = Haml::Buffer.new(@haml_buffer, Haml::Engine.new('').send(:options_for_buffer))
42
41
  nil
43
42
  end
44
43
 
45
44
  # call-seq:
46
- # find_and_preserve(input)
45
+ # non_haml { ... }
46
+ #
47
+ # Runs a block of code in a non-Haml context
48
+ # (i.e. #is_haml? will return false).
49
+ #
50
+ # This is mainly useful for rendering sub-templates such as partials in a non-Haml language,
51
+ # particularly where helpers may behave differently when run from Haml.
52
+ #
53
+ # Note that this is automatically applied to Rails partials.
54
+ def non_haml
55
+ was_active = @haml_buffer.active?
56
+ @haml_buffer.active = false
57
+ res = yield
58
+ @haml_buffer.active = was_active
59
+ res
60
+ end
61
+
62
+ # call-seq:
63
+ # find_and_preserve(input, tags = haml_buffer.options[:preserve])
47
64
  # find_and_preserve {...}
48
- #
49
- # Isolates the whitespace-sensitive tags in the string and uses preserve
50
- # to convert any endlines inside them into HTML entities for endlines.
51
- def find_and_preserve(input = '', &block)
65
+ #
66
+ # Uses preserve to convert any newlines inside whitespace-sensitive tags
67
+ # into the HTML entities for endlines.
68
+ # @tags@ is an array of tags to preserve.
69
+ # It defaults to the value of the <tt>:preserve</tt> option.
70
+ def find_and_preserve(input = '', tags = haml_buffer.options[:preserve], &block)
52
71
  return find_and_preserve(capture_haml(&block)) if block
53
72
 
54
73
  input = input.to_s
55
- input.gsub(/<(textarea|code|pre)([^>]*)>(.*?)(<\/\1>)/im) do
74
+ input.gsub(/<(#{tags.map(&Regexp.method(:escape)).join('|')})([^>]*)>(.*?)(<\/\1>)/im) do
56
75
  "<#{$1}#{$2}>#{preserve($3)}</#{$1}>"
57
76
  end
58
77
  end
@@ -67,7 +86,7 @@ module Haml
67
86
  def preserve(input = '', &block)
68
87
  return preserve(capture_haml(&block)) if block
69
88
 
70
- input.gsub(/\n/, '&#x000A;').gsub(/\r/, '')
89
+ input.chomp("\n").gsub(/\n/, '&#x000A;').gsub(/\r/, '')
71
90
  end
72
91
 
73
92
  alias_method :flatten, :preserve
@@ -107,14 +126,14 @@ module Haml
107
126
  def list_of(array, &block) # :yields: item
108
127
  to_return = array.collect do |i|
109
128
  result = capture_haml(i, &block)
110
-
129
+
111
130
  if result.count("\n") > 1
112
131
  result.gsub!("\n", "\n ")
113
132
  result = "\n #{result.strip}\n"
114
133
  else
115
134
  result.strip!
116
135
  end
117
-
136
+
118
137
  "<li>#{result}</li>"
119
138
  end
120
139
  to_return.join("\n")
@@ -156,14 +175,14 @@ module Haml
156
175
  haml_buffer.tabulation += i
157
176
  end
158
177
 
159
- # Increments the number of tabs the buffer automatically adds
178
+ # Decrements the number of tabs the buffer automatically adds
160
179
  # to the lines of the template.
161
180
  #
162
- # See tab_up.
181
+ # See also tab_up.
163
182
  def tab_down(i = 1)
164
183
  haml_buffer.tabulation -= i
165
184
  end
166
-
185
+
167
186
  # Surrounds the given block of Haml code with the given characters,
168
187
  # with no whitespace in between.
169
188
  # For example:
@@ -187,10 +206,10 @@ module Haml
187
206
  def surround(front, back = nil, &block)
188
207
  back ||= front
189
208
  output = capture_haml(&block)
190
-
209
+
191
210
  "#{front}#{output.chomp}#{back}\n"
192
211
  end
193
-
212
+
194
213
  # Prepends the given character to the beginning of the Haml block,
195
214
  # with no whitespace between.
196
215
  # For example:
@@ -205,7 +224,7 @@ module Haml
205
224
  def precede(char, &block)
206
225
  "#{char}#{capture_haml(&block).chomp}\n"
207
226
  end
208
-
227
+
209
228
  # Appends the given character to the end of the Haml block,
210
229
  # with no whitespace between.
211
230
  # For example:
@@ -222,7 +241,7 @@ module Haml
222
241
  def succeed(char, &block)
223
242
  "#{capture_haml(&block).chomp}#{char}\n"
224
243
  end
225
-
244
+
226
245
  # Captures the result of the given block of Haml code,
227
246
  # gets rid of the excess indentation,
228
247
  # and returns it as a string.
@@ -246,24 +265,24 @@ module Haml
246
265
 
247
266
  #
248
267
  # call-seq:
249
- # open(name, attributes = {}) {...}
250
- # open(name, text, attributes = {}) {...}
268
+ # haml_tag(name, attributes = {}) {...}
269
+ # haml_tag(name, text, attributes = {}) {...}
251
270
  #
252
271
  # Creates an HTML tag with the given name and optionally text and attributes.
253
272
  # Can take a block that will be executed
254
273
  # between when the opening and closing tags are output.
255
274
  # If the block is a Haml block or outputs text using puts,
256
275
  # the text will be properly indented.
257
- #
276
+ #
258
277
  # For example,
259
278
  #
260
- # open :table do
261
- # open :tr do
262
- # open :td, {:class => 'cell'} do
263
- # open :strong, "strong!"
279
+ # haml_tag :table do
280
+ # haml_tag :tr do
281
+ # haml_tag :td, {:class => 'cell'} do
282
+ # haml_tag :strong, "strong!"
264
283
  # puts "data"
265
284
  # end
266
- # open :td do
285
+ # haml_tag :td do
267
286
  # puts "more_data"
268
287
  # end
269
288
  # end
@@ -286,18 +305,23 @@ module Haml
286
305
  # </table>
287
306
  #
288
307
  def haml_tag(name, attributes = {}, alt_atts = {}, &block)
308
+ name = name.to_s
309
+
289
310
  text = nil
290
311
  if attributes.is_a? String
291
312
  text = attributes
292
313
  attributes = alt_atts
293
314
  end
294
315
 
295
- if text.nil? && block.nil?
296
- puts "<#{name}#{Haml::Precompiler.build_attributes(haml_buffer.options[:attr_wrapper], attributes)} />"
316
+ attributes = Haml::Precompiler.build_attributes(
317
+ haml_buffer.html?, haml_buffer.options[:attr_wrapper], attributes)
318
+
319
+ if text.nil? && block.nil? && haml_buffer.options[:autoclose].include?(name)
320
+ puts "<#{name}#{attributes} />"
297
321
  return nil
298
322
  end
299
323
 
300
- puts "<#{name}#{Haml::Precompiler.build_attributes(haml_buffer.options[:attr_wrapper], attributes)}>"
324
+ puts "<#{name}#{attributes}>"
301
325
  unless text && text.empty?
302
326
  tab_up
303
327
  # Print out either the text (using push_text) or call the block and add an endline
@@ -312,22 +336,37 @@ module Haml
312
336
  nil
313
337
  end
314
338
 
315
- def open(*args, &block)
316
- warn <<END
317
- DEPRECATION WARNING:
318
- The Haml #open helper is deprecated and will be removed in version 2.0.
319
- Use the #haml_tag method instead.
320
- END
321
- haml_tag(*args, &block)
339
+ # Characters that need to be escaped to HTML entities from user input
340
+ HTML_ESCAPE = { '&'=>'&amp;', '<'=>'&lt;', '>'=>'&gt;', '"'=>'&quot;', "'"=>'&#039;', }
341
+
342
+ # Returns a copy of <tt>text</tt> with ampersands, angle brackets and quotes
343
+ # escaped into HTML entities.
344
+ def html_escape(text)
345
+ text.to_s.gsub(/[\"><&]/) { |s| HTML_ESCAPE[s] }
346
+ end
347
+
348
+ # Escapes HTML entities in <tt>text</tt>, but without escaping an ampersand
349
+ # that is already part of an escaped entity.
350
+ def escape_once(text)
351
+ text.to_s.gsub(/[\"><]|&(?!([a-zA-Z]+|(#\d+));)/) { |s| HTML_ESCAPE[s] }
322
352
  end
323
-
353
+
354
+ # Returns whether or not the current template is a Haml template.
355
+ #
356
+ # This function, unlike other Haml::Helpers functions,
357
+ # also works in other ActionView templates,
358
+ # where it will always return false.
359
+ def is_haml?
360
+ !@haml_buffer.nil? && @haml_buffer.active?
361
+ end
362
+
324
363
  private
325
364
 
326
365
  # Gets a reference to the current Haml::Buffer object.
327
366
  def haml_buffer
328
- @haml_stack[-1]
367
+ @haml_buffer
329
368
  end
330
-
369
+
331
370
  # Gives a proc the same local "_hamlout" and "_erbout" variables
332
371
  # that the current template has.
333
372
  def haml_bind_proc(&proc)
@@ -335,46 +374,42 @@ END
335
374
  _erbout = _hamlout.buffer
336
375
  proc { |*args| proc.call(*args) }
337
376
  end
338
-
377
+
339
378
  # Performs the function of capture_haml, assuming <tt>local_buffer</tt>
340
379
  # is where the output of block goes.
341
380
  def capture_haml_with_buffer(local_buffer, *args, &block)
342
381
  position = local_buffer.length
343
-
382
+
344
383
  block.call *args
345
-
384
+
346
385
  captured = local_buffer.slice!(position..-1)
347
-
386
+
348
387
  min_tabs = nil
349
388
  captured.each do |line|
350
389
  tabs = line.index(/[^ ]/)
351
390
  min_tabs ||= tabs
352
391
  min_tabs = min_tabs > tabs ? tabs : min_tabs
353
392
  end
354
-
393
+
355
394
  result = captured.map do |line|
356
395
  line[min_tabs..-1]
357
396
  end
358
397
  result.to_s
359
398
  end
360
399
 
361
- # Returns whether or not the current template is a Haml template.
362
- #
363
- # This function, unlike other Haml::Helpers functions,
364
- # also works in other ActionView templates,
365
- # where it will always return false.
366
- def is_haml?
367
- @haml_is_haml
368
- end
369
-
370
400
  include ActionViewExtensions if self.const_defined? "ActionViewExtensions"
371
401
  end
372
402
  end
373
403
 
374
- module ActionView
375
- class Base # :nodoc:
376
- def is_haml?
377
- false
378
- end
404
+ class Object
405
+ # Haml overrides various ActionView helpers,
406
+ # which call an #is_haml? method
407
+ # to determine whether or not the current context object
408
+ # is a proper Haml context.
409
+ # Because ActionView helpers may be included in non-ActionView::Base classes,
410
+ # it's a good idea to define is_haml? for all objects.
411
+ def is_haml?
412
+ false
379
413
  end
380
414
  end
415
+
@@ -31,7 +31,7 @@ if defined?(ActionView)
31
31
  #
32
32
  # .entry
33
33
  # :color #00f
34
- #
34
+ #
35
35
  def page_class
36
36
  controller.controller_name + " " + controller.action_name
37
37
  end
@@ -2,11 +2,8 @@ if defined?(ActionView) and not defined?(Merb::Plugins)
2
2
  module ActionView
3
3
  class Base # :nodoc:
4
4
  def render_with_haml(*args, &block)
5
- was_haml = is_haml?
6
- @haml_is_haml = false
7
- res = render_without_haml(*args, &block)
8
- @haml_is_haml = was_haml
9
- res
5
+ return non_haml { render_without_haml(*args, &block) } if is_haml?
6
+ render_without_haml(*args, &block)
10
7
  end
11
8
  alias_method :render_without_haml, :render
12
9
  alias_method :render, :render_with_haml
@@ -17,6 +14,28 @@ if defined?(ActionView) and not defined?(Merb::Plugins)
17
14
  module Helpers
18
15
  # :stopdoc:
19
16
  module CaptureHelper
17
+ def capture_with_haml(*args, &block)
18
+ # Rails' #capture helper will just return the value of the block
19
+ # if it's not actually in the template context,
20
+ # as detected by the existance of an _erbout variable.
21
+ # We've got to do the same thing for compatibility.
22
+ block_is_haml =
23
+ begin
24
+ eval('_hamlout', block)
25
+ true
26
+ rescue
27
+ false
28
+ end
29
+
30
+ if block_is_haml && is_haml?
31
+ capture_haml(*args, &block)
32
+ else
33
+ capture_without_haml(*args, &block)
34
+ end
35
+ end
36
+ alias_method :capture_without_haml, :capture
37
+ alias_method :capture, :capture_with_haml
38
+
20
39
  def capture_erb_with_buffer_with_haml(*args, &block)
21
40
  if is_haml?
22
41
  capture_haml_with_buffer(*args, &block)
@@ -40,6 +59,35 @@ if defined?(ActionView) and not defined?(Merb::Plugins)
40
59
  alias_method :concat, :concat_with_haml
41
60
  end
42
61
 
62
+ module TagHelper
63
+ def content_tag_with_haml(name, *args, &block)
64
+ content = content_tag_without_haml(name, *args, &block)
65
+
66
+ if is_haml? && haml_buffer.options[:preserve].include?(name.to_s)
67
+ content = Haml::Helpers.preserve content
68
+ end
69
+
70
+ content
71
+ end
72
+ alias_method :content_tag_without_haml, :content_tag
73
+ alias_method :content_tag, :content_tag_with_haml
74
+ end
75
+
76
+ class InstanceTag
77
+ # Includes TagHelper
78
+
79
+ def haml_buffer
80
+ @template_object.send :haml_buffer
81
+ end
82
+
83
+ def is_haml?
84
+ @template_object.send :is_haml?
85
+ end
86
+
87
+ alias_method :content_tag_without_haml, :content_tag
88
+ alias_method :content_tag, :content_tag_with_haml
89
+ end
90
+
43
91
  module FormTagHelper
44
92
  def form_tag_with_haml(url_for_options = {}, options = {}, *parameters_for_url, &proc)
45
93
  if is_haml?
@@ -66,7 +114,7 @@ if defined?(ActionView) and not defined?(Merb::Plugins)
66
114
  module FormHelper
67
115
  def form_for_with_haml(object_name, *args, &proc)
68
116
  if block_given? && is_haml?
69
- oldproc = proc
117
+ oldproc = proc
70
118
  proc = haml_bind_proc do |*args|
71
119
  tab_up
72
120
  oldproc.call(*args)
@@ -59,7 +59,7 @@ module Haml
59
59
  String.new
60
60
  else
61
61
  lines = text.split("\n")
62
-
62
+
63
63
  lines.map do |line|
64
64
  line.strip!
65
65
  "#{tabulate(tabs)}#{'\\' if Haml::Engine::SPECIAL_CHARACTERS.include?(line[0])}#{line}\n"
@@ -73,7 +73,7 @@ module Haml
73
73
  def self.options
74
74
  @@options
75
75
  end
76
-
76
+
77
77
  TEXT_REGEXP = /^(\s*).*$/
78
78
 
79
79
  class ::Hpricot::Doc
@@ -129,14 +129,14 @@ module Haml
129
129
 
130
130
  class ::Hpricot::Elem
131
131
  def to_haml(tabs = 0)
132
- output = "#{tabulate(tabs)}"
132
+ output = "#{tabulate(tabs)}"
133
133
  if HTML.options[:rhtml] && name[0...5] == 'haml:'
134
134
  return output + HTML.send("haml_tag_#{name[5..-1]}",
135
135
  CGI.unescapeHTML(self.innerHTML))
136
136
  end
137
137
 
138
138
  output += "%#{name}" unless name == 'div' && (attributes.include?('id') || attributes.include?('class'))
139
-
139
+
140
140
  if attributes
141
141
  output += "##{attributes['id']}" if attributes['id']
142
142
  attributes['class'].split(' ').each { |c| output += ".#{c}" } if attributes['class']
@@ -144,10 +144,10 @@ module Haml
144
144
  remove_attribute('class')
145
145
  output += haml_attributes if attributes.length > 0
146
146
  end
147
-
147
+
148
148
  output += "/" if children.length == 0
149
149
  output += "\n"
150
-
150
+
151
151
  self.children.each do |child|
152
152
  output += child.to_haml(tabs + 1)
153
153
  end
@@ -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-#\n-#").split(/\n?\r|\r?\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,47 +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? ? ' /' : ''}>"
475
508
  end
476
-
509
+
477
510
  # Parses a line into tag_name, attributes, attributes_hash, object_ref, action, value
478
511
  def parse_tag(line)
479
- raise SyntaxError.new("Invalid tag: \"#{line}\"") unless match = line.scan(/[%]([-:\w]+)([-\w\.\#]*)(.*)/)[0]
512
+ raise SyntaxError.new("Invalid tag: \"#{line}\".") unless match = line.scan(/%([-:\w]+)([-\w\.\#]*)(.*)/)[0]
480
513
  tag_name, attributes, rest = match
481
- if rest[0] == ?{
482
- scanner = StringScanner.new(rest)
483
- attributes_hash, rest = balance(scanner, ?{, ?})
484
- attributes_hash = attributes_hash[1, attributes_hash.length - 2] if attributes_hash
485
- end
514
+ attributes_hash, rest = parse_attributes(rest) if rest[0] == ?{
486
515
  if rest
487
516
  object_ref, rest = balance(rest, ?[, ?]) if rest[0] == ?[
488
- action, value = rest.scan(/([=\/\~]?)?(.*)?/)[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? '<'
489
522
  end
490
523
  value = value.to_s.strip
491
- [tag_name, attributes, attributes_hash, object_ref, action, value]
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
492
533
  end
493
534
 
494
535
  # Parses a line that will render as an XHTML tag, and adds the code that will
495
536
  # render that tag to <tt>@precompiled</tt>.
496
537
  def render_tag(line)
497
- tag_name, attributes, attributes_hash, object_ref, action, value = parse_tag(line)
498
-
538
+ tag_name, attributes, attributes_hash, object_ref, nuke_outer_whitespace,
539
+ nuke_inner_whitespace, action, value = parse_tag(line)
540
+
499
541
  raise SyntaxError.new("Illegal element: classes and ids must have values.") if attributes =~ /[\.#](\.|#|\z)/
500
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
+
501
549
  case action
502
- when '/'; atomic = true
503
- when '~'; parse = flattened = true
550
+ when '/'; self_closing = xhtml?
551
+ when '~'; parse = preserve_script = true
504
552
  when '='
505
553
  parse = true
506
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
507
560
  end
508
-
561
+
509
562
  if parse && @options[:suppress_eval]
510
563
  parse = false
511
564
  value = ''
512
565
  end
513
566
 
567
+ escape_html = (action == '&' || (action != '!' && @options[:escape_html]))
568
+
514
569
  object_ref = "nil" if object_ref.nil? || @options[:suppress_eval]
515
570
 
516
571
  static_attributes = parse_static_hash(attributes_hash) # Try pre-compiling a static attributes hash
@@ -518,41 +573,58 @@ END
518
573
  attributes = parse_class_and_id(attributes)
519
574
  Buffer.merge_attrs(attributes, static_attributes) if static_attributes
520
575
 
521
- raise SyntaxError.new("Illegal Nesting: Nesting within an atomic tag is illegal.") if @block_opened && atomic
522
- raise SyntaxError.new("Illegal Nesting: Content can't be both given on the same line as %#{tag_name} and nested within it.") if @block_opened && !value.empty?
523
- raise SyntaxError.new("Tag has no content.") if parse && value.empty?
524
- 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?
525
580
 
526
- atomic = true if !@block_opened && value.empty? && @options[:autoclose].include?(tag_name)
527
-
528
- if object_ref == "nil" && attributes_hash.nil? && !flattened && (parse || Buffer.one_liner?(value))
529
- # This means that we can render the tag directly to text and not process it in the buffer
530
- tag_closed = !value.empty? && Buffer.one_liner?(value) && !parse
581
+ self_closing ||= !!( !@block_opened && value.empty? && @options[:autoclose].include?(tag_name) )
531
582
 
532
- open_tag = prerender_tag(tag_name, atomic, attributes)
533
- open_tag << "#{value}</#{tag_name}>" if tag_closed
534
- open_tag << "\n" unless parse
583
+ dont_indent_next_line =
584
+ (nuke_outer_whitespace && !@block_opened) ||
585
+ (nuke_inner_whitespace && @block_opened)
535
586
 
536
- push_merged_text(open_tag, tag_closed || atomic ? 0 : 1, 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
590
+
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
537
603
  return if tag_closed
538
604
  else
539
605
  flush_merged_text
540
606
  content = value.empty? || parse ? 'nil' : value.dump
541
607
  attributes_hash = ', ' + attributes_hash if attributes_hash
542
- 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
543
613
  end
544
-
545
- return if atomic
614
+
615
+ return if self_closing
546
616
 
547
617
  if value.empty?
548
- push_and_tabulate([:element, tag_name])
549
- @output_tabs += 1
618
+ push_and_tabulate([:element, [tag_name, nuke_outer_whitespace, nuke_inner_whitespace]])
619
+ @output_tabs += 1 unless nuke_inner_whitespace
550
620
  return
551
621
  end
552
-
622
+
553
623
  if parse
554
624
  flush_merged_text
555
- 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"))
556
628
  end
557
629
  end
558
630
 
@@ -564,78 +636,105 @@ END
564
636
 
565
637
  # Renders an XHTML comment.
566
638
  def render_comment(line)
567
- conditional, content = line.scan(COMMENT_REGEX)[0]
568
- content.strip!
639
+ conditional, line = balance(line, ?[, ?]) if line[0] == ?[
640
+ line.strip!
569
641
  conditional << ">" if conditional
570
-
571
- if @block_opened && !content.empty?
572
- 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)
573
645
  end
574
646
 
575
647
  open = "<!--#{conditional} "
576
-
648
+
577
649
  # Render it statically if possible
578
- if !content.empty? && Buffer.one_liner?(content)
579
- return push_text("#{open}#{content} #{conditional ? "<![endif]-->" : "-->"}")
650
+ unless line.empty?
651
+ return push_text("#{open}#{line} #{conditional ? "<![endif]-->" : "-->"}")
580
652
  end
581
653
 
582
654
  push_text(open, 1)
583
655
  @output_tabs += 1
584
656
  push_and_tabulate([:comment, !conditional.nil?])
585
- unless content.empty?
586
- push_text(content)
657
+ unless line.empty?
658
+ push_text(line)
587
659
  close
588
660
  end
589
661
  end
590
-
662
+
591
663
  # Renders an XHTML doctype or XML shebang.
592
664
  def render_doctype(line)
593
- raise SyntaxError.new("Illegal Nesting: Nesting within a header command is illegal.") if @block_opened
594
- 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
595
668
  end
596
669
 
597
670
  def text_for_doctype(text)
598
671
  text = text[3..-1].lstrip.downcase
599
- if text[0...3] == "xml"
672
+ if text.index("xml") == 0
673
+ return nil if html?
600
674
  wrapper = @options[:attr_wrapper]
601
675
  return "<?xml version=#{wrapper}1.0#{wrapper} encoding=#{wrapper}#{text.split(' ')[1] || "utf-8"}#{wrapper} ?>"
602
676
  end
603
677
 
604
- version, type = text.scan(DOCTYPE_REGEX)[0]
605
- if version == "1.1"
606
- return '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">'
607
- end
678
+ if html5?
679
+ '<!DOCTYPE html>'
680
+ else
681
+ version, type = text.scan(DOCTYPE_REGEX)[0]
608
682
 
609
- case type
610
- when "strict"; return '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">'
611
- when "frameset"; return '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">'
612
- 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
613
701
  end
614
702
  end
615
703
 
616
704
  # Starts a filtered block.
617
705
  def start_filtered(name)
618
- raise SyntaxError.new('Filters must have nested text.') unless @block_opened
706
+ raise Error.new("Invalid filter name \":#{name}\".") unless name =~ /^\w+$/
619
707
 
620
- unless filter = options[:filters][name]
708
+ unless filter = Filters.defined[name]
621
709
  if filter == 'redcloth' || filter == 'markdown' || filter == 'textile'
622
- 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")
623
711
  end
624
- raise HamlError.new("\"#{name}\" filter is not defined!")
712
+ raise Error.new("Filter \"#{name}\" is not defined.")
625
713
  end
626
714
 
627
715
  push_and_tabulate([:filtered, filter])
628
716
  @flat_spaces = @template_tabs * 2
629
717
  @filter_buffer = String.new
718
+ @block_opened = false
719
+ end
720
+
721
+ def contains_interpolation?(str)
722
+ str.include?('#{')
630
723
  end
631
724
 
632
725
  def unescape_interpolation(str)
633
726
  scan = StringScanner.new(str.dump)
634
727
  str = ''
635
728
 
636
- while scan.scan(/(.*?)\\\#\{/)
637
- str << scan.matched[0...-3]
638
- str << eval("\"\\\#{#{balance(scan, ?{, ?}, 1)[0][0...-1]}}\"")
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
639
738
  end
640
739
 
641
740
  str + scan.rest
@@ -654,18 +753,20 @@ END
654
753
 
655
754
  raise SyntaxError.new("Unbalanced brackets.")
656
755
  end
657
-
658
756
 
659
757
  # Counts the tabulation of a line.
660
758
  def count_soft_tabs(line)
661
759
  spaces = line.index(/([^ ]|$)/)
662
760
  if line[spaces] == ?\t
663
761
  return nil if line.strip.empty?
664
- 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
665
766
  end
666
767
  [spaces, spaces/2]
667
768
  end
668
-
769
+
669
770
  # Pushes value onto <tt>@to_close_stack</tt> and increases
670
771
  # <tt>@template_tabs</tt>.
671
772
  def push_and_tabulate(value)
@@ -677,10 +778,30 @@ END
677
778
  @flat_spaces != -1
678
779
  end
679
780
 
680
- def newline(skip_next = false)
681
- return @skip_next_newline = false if @skip_next_newline
682
- @skip_next_newline = true if skip_next
781
+ def newline
782
+ @newlines += 1
783
+ end
784
+
785
+ def newline_now
683
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
684
805
  end
685
806
  end
686
807
  end