mack-haml 0.8.1 → 0.8.2

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