mack-haml 0.8.1 → 0.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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