haml 1.7.2 → 1.8.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 (71) hide show
  1. data/README +17 -9
  2. data/Rakefile +12 -4
  3. data/VERSION +1 -1
  4. data/init.rb +1 -6
  5. data/lib/haml.rb +65 -7
  6. data/lib/haml/buffer.rb +49 -84
  7. data/lib/haml/engine.rb +155 -797
  8. data/lib/haml/error.rb +3 -33
  9. data/lib/haml/exec.rb +86 -65
  10. data/lib/haml/filters.rb +57 -27
  11. data/lib/haml/helpers.rb +52 -9
  12. data/lib/haml/helpers/action_view_mods.rb +1 -1
  13. data/lib/haml/html.rb +20 -5
  14. data/lib/haml/precompiler.rb +671 -0
  15. data/lib/haml/template.rb +20 -73
  16. data/lib/haml/template/patch.rb +51 -0
  17. data/lib/haml/template/plugin.rb +21 -0
  18. data/lib/sass.rb +78 -3
  19. data/lib/sass/constant.rb +45 -19
  20. data/lib/sass/constant.rb.rej +42 -0
  21. data/lib/sass/constant/string.rb +4 -0
  22. data/lib/sass/css.rb +162 -39
  23. data/lib/sass/engine.rb +38 -14
  24. data/lib/sass/plugin.rb +79 -44
  25. data/lib/sass/tree/attr_node.rb +12 -11
  26. data/lib/sass/tree/comment_node.rb +9 -3
  27. data/lib/sass/tree/directive_node.rb +51 -0
  28. data/lib/sass/tree/node.rb +13 -6
  29. data/lib/sass/tree/rule_node.rb +34 -12
  30. data/test/benchmark.rb +85 -52
  31. data/test/haml/engine_test.rb +172 -84
  32. data/test/haml/helper_test.rb +31 -3
  33. data/test/haml/html2haml_test.rb +60 -0
  34. data/test/haml/markaby/standard.mab +52 -0
  35. data/test/haml/results/eval_suppressed.xhtml +4 -1
  36. data/test/haml/results/helpers.xhtml +15 -4
  37. data/test/haml/results/just_stuff.xhtml +9 -1
  38. data/test/haml/results/standard.xhtml +0 -1
  39. data/test/haml/rhtml/_av_partial_1.rhtml +12 -0
  40. data/test/haml/rhtml/_av_partial_2.rhtml +8 -0
  41. data/test/haml/rhtml/action_view.rhtml +62 -0
  42. data/test/haml/rhtml/standard.rhtml +0 -1
  43. data/test/haml/template_test.rb +41 -21
  44. data/test/haml/templates/_av_partial_1.haml +9 -0
  45. data/test/haml/templates/_av_partial_2.haml +5 -0
  46. data/test/haml/templates/action_view.haml +47 -0
  47. data/test/haml/templates/eval_suppressed.haml +1 -0
  48. data/test/haml/templates/helpers.haml +9 -3
  49. data/test/haml/templates/just_stuff.haml +10 -1
  50. data/test/haml/templates/partials.haml +1 -1
  51. data/test/haml/templates/standard.haml +0 -1
  52. data/test/profile.rb +2 -2
  53. data/test/sass/engine_test.rb +113 -3
  54. data/test/sass/engine_test.rb.rej +18 -0
  55. data/test/sass/plugin_test.rb +34 -11
  56. data/test/sass/results/compact.css +1 -1
  57. data/test/sass/results/complex.css +1 -1
  58. data/test/sass/results/compressed.css +1 -0
  59. data/test/sass/results/constants.css +3 -1
  60. data/test/sass/results/expanded.css +2 -1
  61. data/test/sass/results/import.css +2 -0
  62. data/test/sass/results/nested.css +2 -1
  63. data/test/sass/templates/_partial.sass +2 -0
  64. data/test/sass/templates/compact.sass +2 -0
  65. data/test/sass/templates/complex.sass +1 -0
  66. data/test/sass/templates/compressed.sass +15 -0
  67. data/test/sass/templates/constants.sass +9 -0
  68. data/test/sass/templates/expanded.sass +2 -0
  69. data/test/sass/templates/import.sass +1 -1
  70. data/test/sass/templates/nested.sass +2 -0
  71. metadata +22 -2
@@ -44,7 +44,7 @@ if defined?(ActionView) and not defined?(Merb::Plugins)
44
44
  def form_tag_with_haml(url_for_options = {}, options = {}, *parameters_for_url, &proc)
45
45
  if is_haml?
46
46
  if block_given?
47
- oldproc = proc
47
+ oldproc = proc
48
48
  proc = bind_proc do |*args|
49
49
  concat "\n"
50
50
  tab_up
@@ -27,7 +27,9 @@ module Haml
27
27
  match_to_html(template, /<%=(.*?)-?%>/m, 'loud')
28
28
  match_to_html(template, /<%(.*?)-?%>/m, 'silent')
29
29
  end
30
- @template = Hpricot(template)
30
+
31
+ method = @@options[:xhtml] ? Hpricot.method(:XML) : method(:Hpricot)
32
+ @template = method.call(template)
31
33
  end
32
34
  end
33
35
 
@@ -129,7 +131,8 @@ module Haml
129
131
  def to_haml(tabs = 0)
130
132
  output = "#{tabulate(tabs)}"
131
133
  if HTML.options[:rhtml] && name[0...5] == 'haml:'
132
- return output + HTML.send("haml_tag_#{name[5..-1]}", self.innerHTML)
134
+ return output + HTML.send("haml_tag_#{name[5..-1]}",
135
+ CGI.unescapeHTML(self.innerHTML))
133
136
  end
134
137
 
135
138
  output += "%#{name}" unless name == 'div' && (attributes.include?('id') || attributes.include?('class'))
@@ -137,9 +140,9 @@ module Haml
137
140
  if attributes
138
141
  output += "##{attributes['id']}" if attributes['id']
139
142
  attributes['class'].split(' ').each { |c| output += ".#{c}" } if attributes['class']
140
- attributes.delete("id")
141
- attributes.delete("class")
142
- output += attributes.inspect if attributes.length > 0
143
+ remove_attribute('id')
144
+ remove_attribute('class')
145
+ output += haml_attributes if attributes.length > 0
143
146
  end
144
147
 
145
148
  output += "/" if children.length == 0
@@ -151,6 +154,18 @@ module Haml
151
154
 
152
155
  output
153
156
  end
157
+
158
+ private
159
+
160
+ # Returns a string representation of an attributes hash
161
+ # that's prettier than that produced by Hash#inspect
162
+ def haml_attributes
163
+ attrs = attributes.map do |name, value|
164
+ name = name.index(/\W/) ? name.inspect : ":#{name}"
165
+ "#{name} => #{value.inspect}"
166
+ end
167
+ "{ #{attrs.join(', ')} }"
168
+ end
154
169
  end
155
170
 
156
171
  def self.haml_tag_loud(text)
@@ -0,0 +1,671 @@
1
+ require 'strscan'
2
+
3
+ module Haml
4
+ module Precompiler
5
+ # Designates an XHTML/XML element.
6
+ ELEMENT = ?%
7
+
8
+ # Designates a <tt><div></tt> element with the given class.
9
+ DIV_CLASS = ?.
10
+
11
+ # Designates a <tt><div></tt> element with the given id.
12
+ DIV_ID = ?#
13
+
14
+ # Designates an XHTML/XML comment.
15
+ COMMENT = ?/
16
+
17
+ # Designates an XHTML doctype.
18
+ DOCTYPE = ?!
19
+
20
+ # Designates script, the result of which is output.
21
+ SCRIPT = ?=
22
+
23
+ # Designates script, the result of which is flattened and output.
24
+ FLAT_SCRIPT = ?~
25
+
26
+ # Designates script which is run but not output.
27
+ SILENT_SCRIPT = ?-
28
+
29
+ # When following SILENT_SCRIPT, designates a comment that is not output.
30
+ SILENT_COMMENT = ?#
31
+
32
+ # Designates a non-parsed line.
33
+ ESCAPE = ?\\
34
+
35
+ # Designates a block of filtered text.
36
+ FILTER = ?:
37
+
38
+ # Designates a non-parsed line. Not actually a character.
39
+ PLAIN_TEXT = -1
40
+
41
+ # Keeps track of the ASCII values of the characters that begin a
42
+ # specially-interpreted line.
43
+ SPECIAL_CHARACTERS = [
44
+ ELEMENT,
45
+ DIV_CLASS,
46
+ DIV_ID,
47
+ COMMENT,
48
+ DOCTYPE,
49
+ SCRIPT,
50
+ FLAT_SCRIPT,
51
+ SILENT_SCRIPT,
52
+ ESCAPE,
53
+ FILTER
54
+ ]
55
+
56
+ # The value of the character that designates that a line is part
57
+ # of a multiline string.
58
+ MULTILINE_CHAR_VALUE = ?|
59
+
60
+ # Characters that designate that a multiline string may be about
61
+ # to begin.
62
+ MULTILINE_STARTERS = SPECIAL_CHARACTERS - [?/]
63
+
64
+ # Keywords that appear in the middle of a Ruby block with lowered
65
+ # indentation. If a block has been started using indentation,
66
+ # lowering the indentation with one of these won't end the block.
67
+ # For example:
68
+ #
69
+ # - if foo
70
+ # %p yes!
71
+ # - else
72
+ # %p no!
73
+ #
74
+ # The block is ended after <tt>%p no!</tt>, because <tt>else</tt>
75
+ # is a member of this array.
76
+ MID_BLOCK_KEYWORDS = ['else', 'elsif', 'rescue', 'ensure', 'when']
77
+
78
+ # The Regex that matches an HTML comment command.
79
+ COMMENT_REGEX = /\/(\[[\w\s\.]*\])?(.*)/
80
+
81
+ # The Regex that matches a Doctype command.
82
+ DOCTYPE_REGEX = /(\d\.\d)?[\s]*([a-z]*)/i
83
+
84
+ # The Regex that matches an HTML tag command.
85
+ TAG_REGEX = /[%]([-:\w]+)([-\w\.\#]*)(\{.*\})?(\[.*\])?([=\/\~]?)?(.*)?/
86
+
87
+ # The Regex that matches a literal string or symbol value
88
+ LITERAL_VALUE_REGEX = /^\s*(:(\w*)|(('|")([^\\\#'"]*?)\4))\s*$/
89
+
90
+ private
91
+
92
+ # Returns the precompiled string with the preamble and postamble
93
+ def precompiled_with_ambles(local_names)
94
+ preamble = <<END.gsub("\n", ";")
95
+ 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]
100
+ _erbout = _hamlout.buffer
101
+ END
102
+ postamble = <<END.gsub("\n", ";")
103
+ @haml_is_haml = false
104
+ @haml_stack.pop.buffer
105
+ END
106
+ preamble + locals_code(local_names) + @precompiled + postamble
107
+ end
108
+
109
+ def locals_code(names)
110
+ names = names.keys if Hash == names
111
+
112
+ names.map do |name|
113
+ "#{name} = _haml_locals[#{name.to_sym.inspect}] || _haml_locals[#{name.to_s.inspect}]"
114
+ end.join(';') + ';'
115
+ end
116
+
117
+ Line = Struct.new("Line", :text, :unstripped, :index, :spaces, :tabs)
118
+
119
+ def precompile
120
+ @precompiled = ''
121
+ @merged_text = ''
122
+ @tab_change = 0
123
+
124
+ 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
127
+ line.spaces, line.tabs = count_soft_tabs(text)
128
+
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
+ suppress_render = handle_multiline(old_line) unless flat?
144
+
145
+ if old_line.text.nil? || suppress_render
146
+ old_line = line
147
+ newline
148
+ next
149
+ end
150
+
151
+ process_indent(old_line) unless old_line.text.empty?
152
+
153
+ if flat?
154
+ push_flat(old_line)
155
+ old_line = line
156
+ newline
157
+ next
158
+ end
159
+
160
+ if old_line.spaces != old_line.tabs * 2
161
+ raise SyntaxError.new("Illegal Indentation: Only two space characters are allowed as tabulation.")
162
+ end
163
+
164
+ unless old_line.text.empty? || @haml_comment
165
+ process_line(old_line.text, old_line.index, line.tabs > old_line.tabs && !line.text.empty?)
166
+ end
167
+
168
+ if !flat? && line.tabs - old_line.tabs > 1
169
+ raise SyntaxError.new("Illegal Indentation: Indenting more than once per line is illegal.")
170
+ end
171
+ old_line = line
172
+ newline
173
+ end
174
+
175
+ # Close all the open tags
176
+ close until @to_close_stack.empty?
177
+ flush_merged_text
178
+ end
179
+
180
+ # Processes and deals with lowering indentation.
181
+ def process_indent(line)
182
+ return unless line.tabs <= @template_tabs && @template_tabs > 0
183
+
184
+ to_close = @template_tabs - line.tabs
185
+ to_close.times { |i| close unless to_close - 1 - i == 0 && mid_block_keyword?(line.text) }
186
+ end
187
+
188
+ # Processes a single line of Haml.
189
+ #
190
+ # This method doesn't return anything; it simply processes the line and
191
+ # adds the appropriate code to <tt>@precompiled</tt>.
192
+ def process_line(text, index, block_opened)
193
+ @block_opened = block_opened
194
+ @index = index + 1
195
+
196
+ case text[0]
197
+ when DIV_CLASS, DIV_ID; render_div(text)
198
+ when ELEMENT; render_tag(text)
199
+ when COMMENT; render_comment(text)
200
+ when SCRIPT
201
+ return push_script(unescape_interpolation(text[2..-1].strip), false) if text[1] == SCRIPT
202
+ push_script(text[1..-1], false)
203
+ when FLAT_SCRIPT; push_flat_script(text[1..-1])
204
+ when SILENT_SCRIPT
205
+ return start_haml_comment if text[1] == SILENT_COMMENT
206
+
207
+ push_silent(text[1..-1], true)
208
+ newline true
209
+ if (@block_opened && !mid_block_keyword?(text)) || text[1..-1].split(' ', 2)[0] == "case"
210
+ push_and_tabulate([:script])
211
+ end
212
+ when FILTER; start_filtered(text[1..-1].downcase)
213
+ when DOCTYPE
214
+ return render_doctype(text) if text[0...3] == '!!!'
215
+ push_plain text
216
+ when ESCAPE; push_plain text[1..-1]
217
+ else push_plain text
218
+ end
219
+ end
220
+
221
+ # Returns whether or not the text is a silent script text with one
222
+ # of Ruby's mid-block keywords.
223
+ def mid_block_keyword?(text)
224
+ text.length > 2 && text[0] == SILENT_SCRIPT && MID_BLOCK_KEYWORDS.include?(text[1..-1].split[0])
225
+ end
226
+
227
+ # Deals with all the logic of figuring out whether a given line is
228
+ # the beginning, continuation, or end of a multiline sequence.
229
+ #
230
+ # This returns whether or not the line should be
231
+ # rendered normally.
232
+ def handle_multiline(line)
233
+ text = line.text
234
+
235
+ # A multiline string is active, and is being continued
236
+ if is_multiline?(text) && @multiline
237
+ @multiline.text << text[0...-1]
238
+ return true
239
+ end
240
+
241
+ # A multiline string has just been activated, start adding the lines
242
+ if is_multiline?(text) && (MULTILINE_STARTERS.include? text[0])
243
+ @multiline = Line.new text[0...-1], nil, line.index, nil, line.tabs
244
+ process_indent(line)
245
+ return true
246
+ end
247
+
248
+ # A multiline string has just ended, make line into the result
249
+ if @multiline && !line.text.empty?
250
+ process_line(@multiline.text, @multiline.index, line.tabs > @multiline.tabs)
251
+ @multiline = nil
252
+ end
253
+
254
+ return false
255
+ end
256
+
257
+ # Checks whether or not +line+ is in a multiline sequence.
258
+ def is_multiline?(text)
259
+ text && text.length > 1 && text[-1] == MULTILINE_CHAR_VALUE && text[-2] == ?\s
260
+ end
261
+
262
+ # Evaluates <tt>text</tt> in the context of the scope object, but
263
+ # does not output the result.
264
+ def push_silent(text, can_suppress = false)
265
+ flush_merged_text
266
+ return if can_suppress && options[:suppress_eval]
267
+ @precompiled << "#{text};"
268
+ end
269
+
270
+ # Adds <tt>text</tt> to <tt>@buffer</tt> with appropriate tabulation
271
+ # without parsing it.
272
+ def push_merged_text(text, tab_change = 0, try_one_liner = false)
273
+ @merged_text << "#{' ' * @output_tabs}#{text}"
274
+ @tab_change += tab_change
275
+ @try_one_liner = try_one_liner
276
+ 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)
280
+ end
281
+
282
+ def flush_merged_text
283
+ return if @merged_text.empty?
284
+
285
+ @precompiled << "_hamlout.push_text(#{@merged_text.dump}"
286
+ @precompiled << ", #{@tab_change}" if @tab_change != 0 || @try_one_liner
287
+ @precompiled << ");"
288
+ @merged_text = ''
289
+ @tab_change = 0
290
+ @try_one_liner = false
291
+ end
292
+
293
+ # Renders a block of text as plain text.
294
+ # Also checks for an illegally opened block.
295
+ def push_plain(text)
296
+ raise SyntaxError.new("Illegal Nesting: Nesting within plain text is illegal.") if @block_opened
297
+ push_text text
298
+ end
299
+
300
+ # Adds +text+ to <tt>@buffer</tt> while flattening text.
301
+ def push_flat(line)
302
+ tabulation = line.spaces - @flat_spaces
303
+ tabulation = tabulation > -1 ? tabulation : 0
304
+ @filter_buffer << "#{' ' * tabulation}#{line.unstripped}\n"
305
+ end
306
+
307
+ # Causes <tt>text</tt> to be evaluated in the context of
308
+ # the scope object and the result to be added to <tt>@buffer</tt>.
309
+ #
310
+ # If <tt>flattened</tt> is true, Haml::Helpers#find_and_flatten is run on
311
+ # the result before it is added to <tt>@buffer</tt>
312
+ def push_script(text, flattened, close_tag = nil)
313
+ flush_merged_text
314
+ return if options[:suppress_eval]
315
+
316
+ push_silent "haml_temp = #{text}"
317
+ newline true
318
+ out = "haml_temp = _hamlout.push_script(haml_temp, #{flattened.inspect}, #{close_tag.inspect});"
319
+ if @block_opened
320
+ push_and_tabulate([:loud, out])
321
+ else
322
+ @precompiled << out
323
+ end
324
+ end
325
+
326
+ # Causes <tt>text</tt> to be evaluated, and Haml::Helpers#find_and_flatten
327
+ # to be run on it afterwards.
328
+ def push_flat_script(text)
329
+ flush_merged_text
330
+
331
+ raise SyntaxError.new("Tag has no content.") if text.empty?
332
+ push_script(text, true)
333
+ end
334
+
335
+ def start_haml_comment
336
+ return unless @block_opened
337
+
338
+ @haml_comment = true
339
+ push_and_tabulate([:haml_comment])
340
+ end
341
+
342
+ # Closes the most recent item in <tt>@to_close_stack</tt>.
343
+ def close
344
+ tag, value = @to_close_stack.pop
345
+ case tag
346
+ when :script; close_block
347
+ when :comment; close_comment value
348
+ when :element; close_tag value
349
+ when :loud; close_loud value
350
+ when :filtered; close_filtered value
351
+ when :haml_comment; close_haml_comment
352
+ end
353
+ end
354
+
355
+ # Puts a line in <tt>@precompiled</tt> that will add the closing tag of
356
+ # the most recently opened tag.
357
+ def close_tag(tag)
358
+ @output_tabs -= 1
359
+ @template_tabs -= 1
360
+ push_text("</#{tag}>", -1)
361
+ end
362
+
363
+ # Closes a Ruby block.
364
+ def close_block
365
+ push_silent "end", true
366
+ @template_tabs -= 1
367
+ end
368
+
369
+ # Closes a comment.
370
+ def close_comment(has_conditional)
371
+ @output_tabs -= 1
372
+ @template_tabs -= 1
373
+ close_tag = has_conditional ? "<![endif]-->" : "-->"
374
+ push_text(close_tag, -1)
375
+ end
376
+
377
+ # Closes a loud Ruby block.
378
+ def close_loud(command)
379
+ push_silent 'end', true
380
+ @precompiled << command
381
+ @template_tabs -= 1
382
+ end
383
+
384
+ # Closes a filtered block.
385
+ def close_filtered(filter)
386
+ @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
+
395
+ @filter_buffer = nil
396
+ @template_tabs -= 1
397
+ end
398
+
399
+ def close_haml_comment
400
+ @haml_comment = false
401
+ @template_tabs -= 1
402
+ end
403
+
404
+ # Iterates through the classes and ids supplied through <tt>.</tt>
405
+ # and <tt>#</tt> syntax, and returns a hash with them as attributes,
406
+ # that can then be merged with another attributes hash.
407
+ def parse_class_and_id(list)
408
+ attributes = {}
409
+ list.scan(/([#.])([-_a-zA-Z0-9]+)/) do |type, property|
410
+ case type
411
+ when '.'
412
+ if attributes['class']
413
+ attributes['class'] += " "
414
+ else
415
+ attributes['class'] = ""
416
+ end
417
+ attributes['class'] += property
418
+ when '#'; attributes['id'] = property
419
+ end
420
+ end
421
+ attributes
422
+ end
423
+
424
+ def parse_literal_value(text)
425
+ return nil unless text
426
+ text.match(LITERAL_VALUE_REGEX)
427
+
428
+ # $2 holds the value matched by a symbol, but is nil for a string match
429
+ # $5 holds the value matched by a string
430
+ $2 || $5
431
+ end
432
+
433
+ def parse_static_hash(text)
434
+ return {} unless text
435
+
436
+ attributes = {}
437
+ text.split(',').each do |attrib|
438
+ key, value, more = attrib.split('=>')
439
+
440
+ # Make sure the key and value and only the key and value exist
441
+ # Otherwise, it's too complicated or dynamic and we'll defer it to the actual Ruby parser
442
+ key = parse_literal_value key
443
+ value = parse_literal_value value
444
+ return nil if more || key.nil? || value.nil?
445
+
446
+ attributes[key] = value
447
+ end
448
+ attributes
449
+ end
450
+
451
+ # This is a class method so it can be accessed from Buffer.
452
+ def self.build_attributes(attr_wrapper, attributes = {})
453
+ quote_escape = attr_wrapper == '"' ? "&quot;" : "&apos;"
454
+ other_quote_char = attr_wrapper == '"' ? "'" : '"'
455
+
456
+ result = attributes.collect do |attr, value|
457
+ next if value.nil?
458
+
459
+ value = value.to_s
460
+ this_attr_wrapper = attr_wrapper
461
+ if value.include? attr_wrapper
462
+ if value.include? other_quote_char
463
+ value = value.gsub(attr_wrapper, quote_escape)
464
+ else
465
+ this_attr_wrapper = other_quote_char
466
+ end
467
+ end
468
+ " #{attr}=#{this_attr_wrapper}#{value}#{this_attr_wrapper}"
469
+ end
470
+ result.compact.sort.join
471
+ end
472
+
473
+ def prerender_tag(name, atomic, attributes)
474
+ "<#{name}#{Precompiler.build_attributes(@options[:attr_wrapper], attributes)}#{atomic ? ' />' : '>'}"
475
+ end
476
+
477
+ # Parses a line that will render as an XHTML tag, and adds the code that will
478
+ # render that tag to <tt>@precompiled</tt>.
479
+ 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
484
+
485
+ raise SyntaxError.new("Illegal element: classes and ids must have values.") if attributes =~ /[\.#](\.|#|\z)/
486
+
487
+ case action
488
+ when '/'; atomic = true
489
+ when '~'; parse = flattened = true
490
+ when '='
491
+ parse = true
492
+ value = unescape_interpolation(value[1..-1].strip) if value[0] == ?=
493
+ end
494
+
495
+ if parse && @options[:suppress_eval]
496
+ parse = false
497
+ value = ''
498
+ end
499
+
500
+ object_ref = "nil" if object_ref.nil? || @options[:suppress_eval]
501
+
502
+ static_attributes = parse_static_hash(attributes_hash) # Try pre-compiling a static attributes hash
503
+ attributes_hash = nil if static_attributes || @options[:suppress_eval]
504
+ attributes = parse_class_and_id(attributes)
505
+ Buffer.merge_attrs(attributes, static_attributes) if static_attributes
506
+
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?
511
+
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
517
+
518
+ open_tag = prerender_tag(tag_name, atomic, attributes)
519
+ open_tag << "#{value}</#{tag_name}>" if tag_closed
520
+ open_tag << "\n" unless parse
521
+
522
+ push_merged_text(open_tag, tag_closed || atomic ? 0 : 1, parse)
523
+ return if tag_closed
524
+ else
525
+ flush_merged_text
526
+ content = value.empty? || parse ? 'nil' : value.dump
527
+ 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})"
529
+ end
530
+
531
+ return if atomic
532
+
533
+ if value.empty?
534
+ push_and_tabulate([:element, tag_name])
535
+ @output_tabs += 1
536
+ return
537
+ end
538
+
539
+ if parse
540
+ flush_merged_text
541
+ push_script(value, flattened, tag_name)
542
+ end
543
+ end
544
+
545
+ # Renders a line that creates an XHTML tag and has an implicit div because of
546
+ # <tt>.</tt> or <tt>#</tt>.
547
+ def render_div(line)
548
+ render_tag('%div' + line)
549
+ end
550
+
551
+ # Renders an XHTML comment.
552
+ def render_comment(line)
553
+ conditional, content = line.scan(COMMENT_REGEX)[0]
554
+ content.strip!
555
+ 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.')
559
+ end
560
+
561
+ open = "<!--#{conditional} "
562
+
563
+ # Render it statically if possible
564
+ if !content.empty? && Buffer.one_liner?(content)
565
+ return push_text("#{open}#{content} #{conditional ? "<![endif]-->" : "-->"}")
566
+ end
567
+
568
+ push_text(open, 1)
569
+ @output_tabs += 1
570
+ push_and_tabulate([:comment, !conditional.nil?])
571
+ unless content.empty?
572
+ push_text(content)
573
+ close
574
+ end
575
+ end
576
+
577
+ # Renders an XHTML doctype or XML shebang.
578
+ 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)
581
+ end
582
+
583
+ def text_for_doctype(text)
584
+ text = text[3..-1].lstrip.downcase
585
+ if text[0...3] == "xml"
586
+ wrapper = @options[:attr_wrapper]
587
+ return "<?xml version=#{wrapper}1.0#{wrapper} encoding=#{wrapper}#{text.split(' ')[1] || "utf-8"}#{wrapper} ?>"
588
+ end
589
+
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
594
+
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">'
599
+ end
600
+ end
601
+
602
+ # Starts a filtered block.
603
+ 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
612
+
613
+ push_and_tabulate([:filtered, filter])
614
+ @flat_spaces = @template_tabs * 2
615
+ @filter_buffer = String.new
616
+ end
617
+
618
+ def unescape_interpolation(str)
619
+ scan = StringScanner.new(str.dump)
620
+ str = ''
621
+
622
+ while scan.scan(/(.*?)\\\#\{/)
623
+ str << scan.matched[0...-3]
624
+ str << eval("\"\\\#{#{balance_brackets(scan)}}\"")
625
+ end
626
+
627
+ str + scan.rest
628
+ end
629
+
630
+ def balance_brackets(scanner)
631
+ str = ''
632
+ count = 1
633
+
634
+ while scanner.scan(/(.*?)[\{\}]/)
635
+ 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
639
+ end
640
+
641
+ raise SyntaxError.new("Unbalanced brackets.")
642
+ end
643
+
644
+ # Counts the tabulation of a line.
645
+ def count_soft_tabs(line)
646
+ spaces = line.index(/([^ ]|$)/)
647
+ 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.")
650
+ end
651
+ [spaces, spaces/2]
652
+ end
653
+
654
+ # Pushes value onto <tt>@to_close_stack</tt> and increases
655
+ # <tt>@template_tabs</tt>.
656
+ def push_and_tabulate(value)
657
+ @to_close_stack.push(value)
658
+ @template_tabs += 1
659
+ end
660
+
661
+ def flat?
662
+ @flat_spaces != -1
663
+ end
664
+
665
+ def newline(skip_next = false)
666
+ return @skip_next_newline = false if @skip_next_newline
667
+ @skip_next_newline = true if skip_next
668
+ @precompiled << "\n"
669
+ end
670
+ end
671
+ end