honkster-haml 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (176) hide show
  1. data/FAQ +138 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.rdoc +332 -0
  4. data/Rakefile +198 -0
  5. data/VERSION.yml +4 -0
  6. data/bin/css2sass +7 -0
  7. data/bin/haml +9 -0
  8. data/bin/html2haml +7 -0
  9. data/bin/sass +8 -0
  10. data/extra/haml-mode.el +596 -0
  11. data/extra/sass-mode.el +137 -0
  12. data/init.rb +8 -0
  13. data/lib/haml/buffer.rb +255 -0
  14. data/lib/haml/engine.rb +268 -0
  15. data/lib/haml/error.rb +22 -0
  16. data/lib/haml/exec.rb +395 -0
  17. data/lib/haml/filters.rb +276 -0
  18. data/lib/haml/helpers/action_view_extensions.rb +45 -0
  19. data/lib/haml/helpers/action_view_mods.rb +181 -0
  20. data/lib/haml/helpers.rb +468 -0
  21. data/lib/haml/html.rb +218 -0
  22. data/lib/haml/precompiler.rb +901 -0
  23. data/lib/haml/shared.rb +45 -0
  24. data/lib/haml/template/patch.rb +58 -0
  25. data/lib/haml/template/plugin.rb +72 -0
  26. data/lib/haml/template.rb +51 -0
  27. data/lib/haml/util.rb +77 -0
  28. data/lib/haml/version.rb +25 -0
  29. data/lib/haml.rb +1043 -0
  30. data/lib/sass/css.rb +388 -0
  31. data/lib/sass/engine.rb +499 -0
  32. data/lib/sass/environment.rb +33 -0
  33. data/lib/sass/error.rb +35 -0
  34. data/lib/sass/plugin/merb.rb +56 -0
  35. data/lib/sass/plugin/rails.rb +24 -0
  36. data/lib/sass/plugin.rb +203 -0
  37. data/lib/sass/repl.rb +51 -0
  38. data/lib/sass/script/bool.rb +13 -0
  39. data/lib/sass/script/color.rb +97 -0
  40. data/lib/sass/script/funcall.rb +28 -0
  41. data/lib/sass/script/functions.rb +122 -0
  42. data/lib/sass/script/lexer.rb +152 -0
  43. data/lib/sass/script/literal.rb +80 -0
  44. data/lib/sass/script/number.rb +231 -0
  45. data/lib/sass/script/operation.rb +30 -0
  46. data/lib/sass/script/parser.rb +142 -0
  47. data/lib/sass/script/string.rb +18 -0
  48. data/lib/sass/script/unary_operation.rb +21 -0
  49. data/lib/sass/script/variable.rb +20 -0
  50. data/lib/sass/script.rb +38 -0
  51. data/lib/sass/tree/attr_node.rb +64 -0
  52. data/lib/sass/tree/comment_node.rb +34 -0
  53. data/lib/sass/tree/debug_node.rb +22 -0
  54. data/lib/sass/tree/directive_node.rb +50 -0
  55. data/lib/sass/tree/file_node.rb +27 -0
  56. data/lib/sass/tree/for_node.rb +29 -0
  57. data/lib/sass/tree/if_node.rb +27 -0
  58. data/lib/sass/tree/mixin_def_node.rb +18 -0
  59. data/lib/sass/tree/mixin_node.rb +34 -0
  60. data/lib/sass/tree/node.rb +99 -0
  61. data/lib/sass/tree/rule_node.rb +120 -0
  62. data/lib/sass/tree/variable_node.rb +24 -0
  63. data/lib/sass/tree/while_node.rb +20 -0
  64. data/lib/sass.rb +1062 -0
  65. data/rails/init.rb +1 -0
  66. data/test/benchmark.rb +99 -0
  67. data/test/haml/engine_test.rb +782 -0
  68. data/test/haml/helper_test.rb +224 -0
  69. data/test/haml/html2haml_test.rb +92 -0
  70. data/test/haml/markaby/standard.mab +52 -0
  71. data/test/haml/mocks/article.rb +6 -0
  72. data/test/haml/results/content_for_layout.xhtml +15 -0
  73. data/test/haml/results/eval_suppressed.xhtml +9 -0
  74. data/test/haml/results/filters.xhtml +62 -0
  75. data/test/haml/results/helpers.xhtml +93 -0
  76. data/test/haml/results/helpful.xhtml +10 -0
  77. data/test/haml/results/just_stuff.xhtml +68 -0
  78. data/test/haml/results/list.xhtml +12 -0
  79. data/test/haml/results/nuke_inner_whitespace.xhtml +40 -0
  80. data/test/haml/results/nuke_outer_whitespace.xhtml +148 -0
  81. data/test/haml/results/original_engine.xhtml +20 -0
  82. data/test/haml/results/partial_layout.xhtml +5 -0
  83. data/test/haml/results/partials.xhtml +21 -0
  84. data/test/haml/results/render_layout.xhtml +3 -0
  85. data/test/haml/results/silent_script.xhtml +74 -0
  86. data/test/haml/results/standard.xhtml +42 -0
  87. data/test/haml/results/tag_parsing.xhtml +23 -0
  88. data/test/haml/results/very_basic.xhtml +5 -0
  89. data/test/haml/results/whitespace_handling.xhtml +89 -0
  90. data/test/haml/rhtml/_av_partial_1.rhtml +12 -0
  91. data/test/haml/rhtml/_av_partial_2.rhtml +8 -0
  92. data/test/haml/rhtml/action_view.rhtml +62 -0
  93. data/test/haml/rhtml/standard.rhtml +54 -0
  94. data/test/haml/template_test.rb +204 -0
  95. data/test/haml/templates/_av_partial_1.haml +9 -0
  96. data/test/haml/templates/_av_partial_1_ugly.haml +9 -0
  97. data/test/haml/templates/_av_partial_2.haml +5 -0
  98. data/test/haml/templates/_av_partial_2_ugly.haml +5 -0
  99. data/test/haml/templates/_layout.erb +3 -0
  100. data/test/haml/templates/_layout_for_partial.haml +3 -0
  101. data/test/haml/templates/_partial.haml +8 -0
  102. data/test/haml/templates/_text_area.haml +3 -0
  103. data/test/haml/templates/action_view.haml +47 -0
  104. data/test/haml/templates/action_view_ugly.haml +47 -0
  105. data/test/haml/templates/breakage.haml +8 -0
  106. data/test/haml/templates/content_for_layout.haml +10 -0
  107. data/test/haml/templates/eval_suppressed.haml +11 -0
  108. data/test/haml/templates/filters.haml +66 -0
  109. data/test/haml/templates/helpers.haml +95 -0
  110. data/test/haml/templates/helpful.haml +11 -0
  111. data/test/haml/templates/just_stuff.haml +83 -0
  112. data/test/haml/templates/list.haml +12 -0
  113. data/test/haml/templates/nuke_inner_whitespace.haml +32 -0
  114. data/test/haml/templates/nuke_outer_whitespace.haml +144 -0
  115. data/test/haml/templates/original_engine.haml +17 -0
  116. data/test/haml/templates/partial_layout.haml +3 -0
  117. data/test/haml/templates/partialize.haml +1 -0
  118. data/test/haml/templates/partials.haml +12 -0
  119. data/test/haml/templates/render_layout.haml +2 -0
  120. data/test/haml/templates/silent_script.haml +40 -0
  121. data/test/haml/templates/standard.haml +42 -0
  122. data/test/haml/templates/standard_ugly.haml +1 -0
  123. data/test/haml/templates/tag_parsing.haml +21 -0
  124. data/test/haml/templates/very_basic.haml +4 -0
  125. data/test/haml/templates/whitespace_handling.haml +87 -0
  126. data/test/linked_rails.rb +12 -0
  127. data/test/sass/css2sass_test.rb +193 -0
  128. data/test/sass/engine_test.rb +655 -0
  129. data/test/sass/functions_test.rb +96 -0
  130. data/test/sass/more_results/more1.css +9 -0
  131. data/test/sass/more_results/more1_with_line_comments.css +26 -0
  132. data/test/sass/more_results/more_import.css +29 -0
  133. data/test/sass/more_templates/_more_partial.sass +2 -0
  134. data/test/sass/more_templates/more1.sass +23 -0
  135. data/test/sass/more_templates/more_import.sass +11 -0
  136. data/test/sass/plugin_test.rb +208 -0
  137. data/test/sass/results/alt.css +4 -0
  138. data/test/sass/results/basic.css +9 -0
  139. data/test/sass/results/compact.css +5 -0
  140. data/test/sass/results/complex.css +87 -0
  141. data/test/sass/results/compressed.css +1 -0
  142. data/test/sass/results/expanded.css +19 -0
  143. data/test/sass/results/import.css +29 -0
  144. data/test/sass/results/line_numbers.css +49 -0
  145. data/test/sass/results/mixins.css +95 -0
  146. data/test/sass/results/multiline.css +24 -0
  147. data/test/sass/results/nested.css +22 -0
  148. data/test/sass/results/parent_ref.css +13 -0
  149. data/test/sass/results/script.css +16 -0
  150. data/test/sass/results/subdir/nested_subdir/nested_subdir.css +1 -0
  151. data/test/sass/results/subdir/subdir.css +3 -0
  152. data/test/sass/results/units.css +11 -0
  153. data/test/sass/script_test.rb +250 -0
  154. data/test/sass/templates/_partial.sass +2 -0
  155. data/test/sass/templates/alt.sass +16 -0
  156. data/test/sass/templates/basic.sass +23 -0
  157. data/test/sass/templates/bork.sass +2 -0
  158. data/test/sass/templates/bork2.sass +2 -0
  159. data/test/sass/templates/compact.sass +17 -0
  160. data/test/sass/templates/complex.sass +309 -0
  161. data/test/sass/templates/compressed.sass +15 -0
  162. data/test/sass/templates/expanded.sass +17 -0
  163. data/test/sass/templates/import.sass +11 -0
  164. data/test/sass/templates/importee.sass +19 -0
  165. data/test/sass/templates/line_numbers.sass +13 -0
  166. data/test/sass/templates/mixins.sass +76 -0
  167. data/test/sass/templates/multiline.sass +20 -0
  168. data/test/sass/templates/nested.sass +25 -0
  169. data/test/sass/templates/parent_ref.sass +25 -0
  170. data/test/sass/templates/script.sass +101 -0
  171. data/test/sass/templates/subdir/nested_subdir/_nested_partial.sass +2 -0
  172. data/test/sass/templates/subdir/nested_subdir/nested_subdir.sass +3 -0
  173. data/test/sass/templates/subdir/subdir.sass +6 -0
  174. data/test/sass/templates/units.sass +11 -0
  175. data/test/test_helper.rb +21 -0
  176. metadata +272 -0
@@ -0,0 +1,901 @@
1
+ require 'strscan'
2
+ require 'haml/shared'
3
+
4
+ module Haml
5
+ module Precompiler
6
+ include Haml::Util
7
+
8
+ # Designates an XHTML/XML element.
9
+ ELEMENT = ?%
10
+
11
+ # Designates a <tt><div></tt> element with the given class.
12
+ DIV_CLASS = ?.
13
+
14
+ # Designates a <tt><div></tt> element with the given id.
15
+ DIV_ID = ?#
16
+
17
+ # Designates an XHTML/XML comment.
18
+ COMMENT = ?/
19
+
20
+ # Designates an XHTML doctype or script that is never HTML-escaped.
21
+ DOCTYPE = ?!
22
+
23
+ # Designates script, the result of which is output.
24
+ SCRIPT = ?=
25
+
26
+ # Designates script that is always HTML-escaped.
27
+ SANITIZE = ?&
28
+
29
+ # Designates script, the result of which is flattened and output.
30
+ FLAT_SCRIPT = ?~
31
+
32
+ # Designates script which is run but not output.
33
+ SILENT_SCRIPT = ?-
34
+
35
+ # When following SILENT_SCRIPT, designates a comment that is not output.
36
+ SILENT_COMMENT = ?#
37
+
38
+ # Designates a non-parsed line.
39
+ ESCAPE = ?\\
40
+
41
+ # Designates a block of filtered text.
42
+ FILTER = ?:
43
+
44
+ # Designates a non-parsed line. Not actually a character.
45
+ PLAIN_TEXT = -1
46
+
47
+ # Keeps track of the ASCII values of the characters that begin a
48
+ # specially-interpreted line.
49
+ SPECIAL_CHARACTERS = [
50
+ ELEMENT,
51
+ DIV_CLASS,
52
+ DIV_ID,
53
+ COMMENT,
54
+ DOCTYPE,
55
+ SCRIPT,
56
+ SANITIZE,
57
+ FLAT_SCRIPT,
58
+ SILENT_SCRIPT,
59
+ ESCAPE,
60
+ FILTER
61
+ ]
62
+
63
+ # The value of the character that designates that a line is part
64
+ # of a multiline string.
65
+ MULTILINE_CHAR_VALUE = ?|
66
+
67
+ # Regex to match keywords that appear in the middle of a Ruby block
68
+ # with lowered indentation.
69
+ # 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_KEYWORD_REGEX = /-\s*(#{%w[else elsif rescue ensure when end].join('|')})\b/
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
+ __in_erb_template = true
97
+ END
98
+ postamble = <<END.gsub("\n", ";")
99
+ @haml_buffer = @haml_buffer.upper
100
+ _erbout
101
+ END
102
+ preamble + locals_code(local_names) + @precompiled + postamble
103
+ end
104
+
105
+ def locals_code(names)
106
+ names = names.keys if Hash == names
107
+
108
+ names.map do |name|
109
+ # Can't use || because someone might explicitly pass in false with a symbol
110
+ sym_local = "_haml_locals[#{name.to_sym.inspect}]"
111
+ str_local = "_haml_locals[#{name.to_s.inspect}]"
112
+ "#{name} = #{sym_local}.nil? ? #{str_local} : #{sym_local}"
113
+ end.join(';') + ';'
114
+ end
115
+
116
+ class Line < Struct.new(:text, :unstripped, :full, :index, :precompiler, :eod)
117
+ alias_method :eod?, :eod
118
+
119
+ def tabs
120
+ line = self
121
+ @tabs ||= precompiler.instance_eval do
122
+ break 0 if line.text.empty? || !(whitespace = line.full[/^\s+/])
123
+
124
+ if @indentation.nil?
125
+ @indentation = whitespace
126
+
127
+ if @indentation.include?(?\s) && @indentation.include?(?\t)
128
+ raise SyntaxError.new("Indentation can't use both tabs and spaces.", line.index)
129
+ end
130
+
131
+ @flat_spaces = @indentation * @template_tabs if flat?
132
+ break 1
133
+ end
134
+
135
+ tabs = whitespace.length / @indentation.length
136
+ break tabs if whitespace == @indentation * tabs
137
+ break @template_tabs if flat? && whitespace =~ /^#{@indentation * @template_tabs}/
138
+
139
+ raise SyntaxError.new(<<END.strip.gsub("\n", ' '), line.index)
140
+ Inconsistent indentation: #{Haml::Shared.human_indentation whitespace, true} used for indentation,
141
+ but the rest of the document was indented using #{Haml::Shared.human_indentation @indentation}.
142
+ END
143
+ end
144
+ end
145
+ end
146
+
147
+ def precompile
148
+ @haml_comment = @dont_indent_next_line = @dont_tab_up_next_text = false
149
+ @indentation = nil
150
+ @line = next_line
151
+ resolve_newlines
152
+ newline
153
+
154
+ raise SyntaxError.new("Indenting at the beginning of the document is illegal.", @line.index) if @line.tabs != 0
155
+
156
+ while next_line
157
+ process_indent(@line) unless @line.text.empty?
158
+
159
+ if flat?
160
+ push_flat(@line)
161
+ @line = @next_line
162
+ newline
163
+ next
164
+ end
165
+
166
+ process_line(@line.text, @line.index) unless @line.text.empty? || @haml_comment
167
+
168
+ if !flat? && @next_line.tabs - @line.tabs > 1
169
+ raise SyntaxError.new("The line was indented #{@next_line.tabs - @line.tabs} levels deeper than the previous line.", @next_line.index)
170
+ end
171
+
172
+ resolve_newlines unless @next_line.eod?
173
+ @line = @next_line
174
+ newline unless @next_line.eod?
175
+ end
176
+
177
+ # Close all the open tags
178
+ close until @to_close_stack.empty?
179
+ flush_merged_text
180
+ end
181
+
182
+ # Processes and deals with lowering indentation.
183
+ def process_indent(line)
184
+ return unless line.tabs <= @template_tabs && @template_tabs > 0
185
+
186
+ to_close = @template_tabs - line.tabs
187
+ to_close.times { |i| close unless to_close - 1 - i == 0 && mid_block_keyword?(line.text) }
188
+ end
189
+
190
+ # Processes a single line of Haml.
191
+ #
192
+ # This method doesn't return anything; it simply processes the line and
193
+ # adds the appropriate code to <tt>@precompiled</tt>.
194
+ def process_line(text, index)
195
+ @index = index + 1
196
+
197
+ case text[0]
198
+ when DIV_CLASS, DIV_ID; render_div(text)
199
+ when ELEMENT; render_tag(text)
200
+ when COMMENT; render_comment(text[1..-1].strip)
201
+ when SANITIZE
202
+ return push_script(unescape_interpolation(text[3..-1].strip), :escape_html => true) if text[1..2] == "=="
203
+ return push_script(text[2..-1].strip, :escape_html => true) if text[1] == SCRIPT
204
+ return push_script(unescape_interpolation(text[1..-1].strip), :escape_html => true) if text[1] == ?\s
205
+ push_plain text
206
+ when SCRIPT
207
+ return push_script(unescape_interpolation(text[2..-1].strip)) if text[1] == SCRIPT
208
+ return push_script(text[1..-1], :escape_html => true) if options[:escape_html]
209
+ push_script(text[1..-1])
210
+ when FLAT_SCRIPT; push_flat_script(text[1..-1])
211
+ when SILENT_SCRIPT
212
+ return start_haml_comment if text[1] == SILENT_COMMENT
213
+
214
+ raise SyntaxError.new(<<END.rstrip, index) if text[1..-1].strip == "end"
215
+ You don't need to use "- end" in Haml. Use indentation instead:
216
+ - if foo?
217
+ %strong Foo!
218
+ - else
219
+ Not foo.
220
+ END
221
+
222
+ push_silent(text[1..-1], true)
223
+ newline_now
224
+
225
+ # Handle stuff like - end.join("|")
226
+ @to_close_stack.first << false if text =~ /-\s*end\b/ && !block_opened?
227
+
228
+ case_stmt = text =~ /-\s*case\b/
229
+ block = block_opened? && !mid_block_keyword?(text)
230
+ push_and_tabulate([:script]) if block || case_stmt
231
+ push_and_tabulate(:nil) if block && case_stmt
232
+ when FILTER; start_filtered(text[1..-1].downcase)
233
+ when DOCTYPE
234
+ return render_doctype(text) if text[0...3] == '!!!'
235
+ return push_script(unescape_interpolation(text[3..-1].strip)) if text[1..2] == "=="
236
+ return push_script(text[2..-1].strip) if text[1] == SCRIPT
237
+ return push_script(unescape_interpolation(text[1..-1].strip)) if text[1] == ?\s
238
+ push_plain text
239
+ when ESCAPE; push_plain text[1..-1]
240
+ else push_plain text
241
+ end
242
+ end
243
+
244
+ # Returns whether or not the text is a silent script text with one
245
+ # of Ruby's mid-block keywords.
246
+ def mid_block_keyword?(text)
247
+ MID_BLOCK_KEYWORD_REGEX =~ text
248
+ end
249
+
250
+ # Evaluates <tt>text</tt> in the context of the scope object, but
251
+ # does not output the result.
252
+ def push_silent(text, can_suppress = false)
253
+ flush_merged_text
254
+ return if can_suppress && options[:suppress_eval]
255
+ @precompiled << "#{text};"
256
+ end
257
+
258
+ # Adds <tt>text</tt> to <tt>@buffer</tt> with appropriate tabulation
259
+ # without parsing it.
260
+ def push_merged_text(text, tab_change = 0, indent = true)
261
+ text = !indent || @dont_indent_next_line || @options[:ugly] ? text : "#{' ' * @output_tabs}#{text}"
262
+ @to_merge << [:text, text, tab_change]
263
+ @dont_indent_next_line = false
264
+ end
265
+
266
+ # Concatenate <tt>text</tt> to <tt>@buffer</tt> without tabulation.
267
+ def concat_merged_text(text)
268
+ @to_merge << [:text, text, 0]
269
+ end
270
+
271
+ def push_text(text, tab_change = 0)
272
+ push_merged_text("#{text}\n", tab_change)
273
+ end
274
+
275
+ def flush_merged_text
276
+ return if @to_merge.empty?
277
+
278
+ text, tab_change = @to_merge.inject(["", 0]) do |(str, mtabs), (type, val, tabs)|
279
+ case type
280
+ when :text
281
+ [str << val.gsub('#{', "\\\#{"), mtabs + tabs]
282
+ when :script
283
+ if mtabs != 0 && !@options[:ugly]
284
+ val = "_hamlout.adjust_tabs(#{mtabs}); " + val
285
+ end
286
+ [str << "\#{#{val}}", 0]
287
+ else
288
+ raise SyntaxError.new("[HAML BUG] Undefined entry in Haml::Precompiler@to_merge.")
289
+ end
290
+ end
291
+
292
+ @precompiled <<
293
+ if @options[:ugly]
294
+ "_erbout << #{unescape_interpolation(text)};"
295
+ else
296
+ "_hamlout.push_text(#{unescape_interpolation(text)}, #{tab_change}, #{@dont_tab_up_next_text.inspect});"
297
+ end
298
+ @to_merge = []
299
+ @dont_tab_up_next_text = false
300
+ end
301
+
302
+ # Renders a block of text as plain text.
303
+ # Also checks for an illegally opened block.
304
+ def push_plain(text)
305
+ if block_opened?
306
+ raise SyntaxError.new("Illegal nesting: nesting within plain text is illegal.", @next_line.index)
307
+ end
308
+
309
+ if contains_interpolation?(text)
310
+ push_script unescape_interpolation(text)
311
+ else
312
+ push_text text
313
+ end
314
+ end
315
+
316
+ # Adds +text+ to <tt>@buffer</tt> while flattening text.
317
+ def push_flat(line)
318
+ text = line.full.dup
319
+ text = "" unless text.gsub!(/^#{@flat_spaces}/, '')
320
+ @filter_buffer << "#{text}\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>opts[: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, opts = {})
329
+ raise SyntaxError.new("There's no Ruby code for = to evaluate.") if text.empty?
330
+ return if options[:suppress_eval]
331
+
332
+ args = %w[preserve_script in_tag preserve_tag escape_html nuke_inner_whitespace]
333
+ args.map! {|name| opts[name.to_sym]}
334
+ args << !block_opened? << @options[:ugly]
335
+
336
+ no_format = @options[:ugly] &&
337
+ !(opts[:preserve_script] || opts[:preserve_tag] || opts[:escape_html])
338
+ output_temp = "(haml_very_temp = haml_temp; haml_temp = nil; haml_very_temp)"
339
+ out = "_hamlout.#{static_method_name(:format_script, *args)}(#{output_temp});"
340
+
341
+ # Prerender tabulation unless we're in a tag
342
+ push_merged_text '' unless opts[:in_tag]
343
+
344
+ unless block_opened?
345
+ @to_merge << [:script, no_format ? "#{text}\n" : "haml_temp = #{text}\n#{out}"]
346
+ concat_merged_text("\n") unless opts[:in_tag] || opts[:nuke_inner_whitespace]
347
+ @newlines -= 1
348
+ return
349
+ end
350
+
351
+ flush_merged_text
352
+
353
+ push_silent "haml_temp = #{text}"
354
+ newline_now
355
+ push_and_tabulate([:loud, "_erbout << #{no_format ? "#{output_temp}.to_s;" : out}",
356
+ !(opts[:in_tag] || opts[:nuke_inner_whitespace] || @options[:ugly])])
357
+ end
358
+
359
+ # Causes <tt>text</tt> to be evaluated, and Haml::Helpers#find_and_flatten
360
+ # to be run on it afterwards.
361
+ def push_flat_script(text)
362
+ flush_merged_text
363
+
364
+ raise SyntaxError.new("There's no Ruby code for ~ to evaluate.") if text.empty?
365
+ push_script(text, :preserve_script => true)
366
+ end
367
+
368
+ def start_haml_comment
369
+ return unless block_opened?
370
+
371
+ @haml_comment = true
372
+ push_and_tabulate([:haml_comment])
373
+ end
374
+
375
+ # Closes the most recent item in <tt>@to_close_stack</tt>.
376
+ def close
377
+ tag, *rest = @to_close_stack.pop
378
+ send("close_#{tag}", *rest)
379
+ end
380
+
381
+ # Puts a line in <tt>@precompiled</tt> that will add the closing tag of
382
+ # the most recently opened tag.
383
+ def close_element(value)
384
+ tag, nuke_outer_whitespace, nuke_inner_whitespace = value
385
+ @output_tabs -= 1 unless nuke_inner_whitespace
386
+ @template_tabs -= 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
391
+ end
392
+
393
+ # Closes a Ruby block.
394
+ def close_script
395
+ push_silent "end", true
396
+ @template_tabs -= 1
397
+ end
398
+
399
+ # Closes a comment.
400
+ def close_comment(has_conditional)
401
+ @output_tabs -= 1
402
+ @template_tabs -= 1
403
+ close_tag = has_conditional ? "<![endif]-->" : "-->"
404
+ push_text(close_tag, -1)
405
+ end
406
+
407
+ # Closes a loud Ruby block.
408
+ def close_loud(command, add_newline, push_end = true)
409
+ push_silent('end', true) if push_end
410
+ @precompiled << command
411
+ @template_tabs -= 1
412
+ concat_merged_text("\n") if add_newline
413
+ end
414
+
415
+ # Closes a filtered block.
416
+ def close_filtered(filter)
417
+ filter.internal_compile(self, @filter_buffer)
418
+ @flat = false
419
+ @flat_spaces = nil
420
+ @filter_buffer = nil
421
+ @template_tabs -= 1
422
+ end
423
+
424
+ def close_haml_comment
425
+ @haml_comment = false
426
+ @template_tabs -= 1
427
+ end
428
+
429
+ def close_nil
430
+ @template_tabs -= 1
431
+ end
432
+
433
+ # Iterates through the classes and ids supplied through <tt>.</tt>
434
+ # and <tt>#</tt> syntax, and returns a hash with them as attributes,
435
+ # that can then be merged with another attributes hash.
436
+ def parse_class_and_id(list)
437
+ attributes = {}
438
+ list.scan(/([#.])([-_a-zA-Z0-9]+)/) do |type, property|
439
+ case type
440
+ when '.'
441
+ if attributes['class']
442
+ attributes['class'] += " "
443
+ else
444
+ attributes['class'] = ""
445
+ end
446
+ attributes['class'] += property
447
+ when '#'; attributes['id'] = property
448
+ end
449
+ end
450
+ attributes
451
+ end
452
+
453
+ def parse_literal_value(text)
454
+ return nil unless text
455
+ text.match(LITERAL_VALUE_REGEX)
456
+
457
+ # $2 holds the value matched by a symbol, but is nil for a string match
458
+ # $5 holds the value matched by a string
459
+ $2 || $5
460
+ end
461
+
462
+ def parse_static_hash(text)
463
+ return {} unless text
464
+
465
+ attributes = {}
466
+ text.split(',').each do |attrib|
467
+ key, value, more = attrib.split('=>')
468
+
469
+ # Make sure the key and value and only the key and value exist
470
+ # Otherwise, it's too complicated or dynamic and we'll defer it to the actual Ruby parser
471
+ key = parse_literal_value key
472
+ value = parse_literal_value value
473
+ return nil if more || key.nil? || value.nil?
474
+
475
+ attributes[key] = value
476
+ end
477
+ text.count("\n").times { newline }
478
+ attributes
479
+ end
480
+
481
+ # This is a class method so it can be accessed from Buffer.
482
+ def self.build_attributes(is_html, attr_wrapper, attributes = {})
483
+ quote_escape = attr_wrapper == '"' ? "&quot;" : "&apos;"
484
+ other_quote_char = attr_wrapper == '"' ? "'" : '"'
485
+
486
+ result = attributes.collect do |attr, value|
487
+ next if value.nil?
488
+
489
+ if value == true
490
+ next " #{attr}" if is_html
491
+ next " #{attr}=#{attr_wrapper}#{attr}#{attr_wrapper}"
492
+ elsif value == false
493
+ next
494
+ end
495
+
496
+ value = Haml::Helpers.preserve(Haml::Helpers.escape_once(value.to_s))
497
+ # We want to decide whether or not to escape quotes
498
+ value.gsub!('&quot;', '"')
499
+ this_attr_wrapper = attr_wrapper
500
+ if value.include? attr_wrapper
501
+ if value.include? other_quote_char
502
+ value = value.gsub(attr_wrapper, quote_escape)
503
+ else
504
+ this_attr_wrapper = other_quote_char
505
+ end
506
+ end
507
+ " #{attr}=#{this_attr_wrapper}#{value}#{this_attr_wrapper}"
508
+ end
509
+ result.compact.sort.join
510
+ end
511
+
512
+ def prerender_tag(name, self_close, attributes)
513
+ attributes_string = Precompiler.build_attributes(html?, @options[:attr_wrapper], attributes)
514
+ "<#{name}#{attributes_string}#{self_close && xhtml? ? ' /' : ''}>"
515
+ end
516
+
517
+ # Parses a line into tag_name, attributes, attributes_hash, object_ref, action, value
518
+ def parse_tag(line)
519
+ raise SyntaxError.new("Invalid tag: \"#{line}\".") unless match = line.scan(/%([-:\w]+)([-\w\.\#]*)(.*)/)[0]
520
+ tag_name, attributes, rest = match
521
+ attributes_hash, rest, last_line = parse_attributes(rest) if rest[0] == ?{
522
+ if rest
523
+ object_ref, rest = balance(rest, ?[, ?]) if rest[0] == ?[
524
+ attributes_hash, rest, last_line = parse_attributes(rest) if rest[0] == ?{ && attributes_hash.nil?
525
+ nuke_whitespace, action, value = rest.scan(/(<>|><|[><])?([=\/\~&!])?(.*)?/)[0]
526
+ nuke_whitespace ||= ''
527
+ nuke_outer_whitespace = nuke_whitespace.include? '>'
528
+ nuke_inner_whitespace = nuke_whitespace.include? '<'
529
+ end
530
+ value = value.to_s.strip
531
+ [tag_name, attributes, attributes_hash, object_ref, nuke_outer_whitespace,
532
+ nuke_inner_whitespace, action, value, last_line || @index]
533
+ end
534
+
535
+ def parse_attributes(line)
536
+ line = line.dup
537
+ last_line = @index
538
+
539
+ begin
540
+ attributes_hash, rest = balance(line, ?{, ?})
541
+ rescue SyntaxError => e
542
+ if line.strip[-1] == ?, && e.message == "Unbalanced brackets."
543
+ line << "\n" << @next_line.text
544
+ last_line += 1
545
+ next_line
546
+ retry
547
+ end
548
+
549
+ raise e
550
+ end
551
+
552
+ attributes_hash = attributes_hash[1...-1] if attributes_hash
553
+ return attributes_hash, rest, last_line
554
+ end
555
+
556
+ # Parses a line that will render as an XHTML tag, and adds the code that will
557
+ # render that tag to <tt>@precompiled</tt>.
558
+ def render_tag(line)
559
+ tag_name, attributes, attributes_hash, object_ref, nuke_outer_whitespace,
560
+ nuke_inner_whitespace, action, value, last_line = parse_tag(line)
561
+
562
+ raise SyntaxError.new("Illegal element: classes and ids must have values.") if attributes =~ /[\.#](\.|#|\z)/
563
+
564
+ # Get rid of whitespace outside of the tag if we need to
565
+ rstrip_buffer! if nuke_outer_whitespace
566
+
567
+ preserve_tag = options[:preserve].include?(tag_name)
568
+ nuke_inner_whitespace ||= preserve_tag
569
+ preserve_tag &&= !options[:ugly]
570
+
571
+ case action
572
+ when '/'; self_closing = true
573
+ when '~'; parse = preserve_script = true
574
+ when '='
575
+ parse = true
576
+ value = unescape_interpolation(value[1..-1].strip) if value[0] == ?=
577
+ when '&', '!'
578
+ if value[0] == ?=
579
+ parse = true
580
+ value =
581
+ if value[1] == ?=
582
+ unescape_interpolation(value[2..-1].strip)
583
+ else
584
+ value[1..-1].strip
585
+ end
586
+ elsif contains_interpolation?(value)
587
+ parse = true
588
+ value = unescape_interpolation(value)
589
+ end
590
+ else
591
+ if contains_interpolation?(value)
592
+ parse = true
593
+ value = unescape_interpolation(value)
594
+ end
595
+ end
596
+
597
+ if parse && @options[:suppress_eval]
598
+ parse = false
599
+ value = ''
600
+ end
601
+
602
+ escape_html = (action == '&' || (action != '!' && @options[:escape_html]))
603
+
604
+ object_ref = "nil" if object_ref.nil? || @options[:suppress_eval]
605
+
606
+ static_attributes = parse_static_hash(attributes_hash) # Try pre-compiling a static attributes hash
607
+ attributes_hash = nil if static_attributes || @options[:suppress_eval]
608
+ attributes = parse_class_and_id(attributes)
609
+ Buffer.merge_attrs(attributes, static_attributes) if static_attributes
610
+
611
+ raise SyntaxError.new("Illegal nesting: nesting within a self-closing tag is illegal.", @next_line.index) if block_opened? && self_closing
612
+ 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?
613
+ raise SyntaxError.new("There's no Ruby code for #{action} to evaluate.", last_line - 1) if parse && value.empty?
614
+ raise SyntaxError.new("Self-closing tags can't have content.", last_line - 1) if self_closing && !value.empty?
615
+
616
+ self_closing ||= !!( !block_opened? && value.empty? && @options[:autoclose].include?(tag_name) )
617
+
618
+ dont_indent_next_line =
619
+ (nuke_outer_whitespace && !block_opened?) ||
620
+ (nuke_inner_whitespace && block_opened?)
621
+
622
+ # Check if we can render the tag directly to text and not process it in the buffer
623
+ if object_ref == "nil" && attributes_hash.nil? && !preserve_script
624
+ tag_closed = !block_opened? && !self_closing && !parse
625
+
626
+ open_tag = prerender_tag(tag_name, self_closing, attributes)
627
+ if tag_closed
628
+ open_tag << "#{value}</#{tag_name}>"
629
+ open_tag << "\n" unless nuke_outer_whitespace
630
+ else
631
+ open_tag << "\n" unless parse || nuke_inner_whitespace || (self_closing && nuke_outer_whitespace)
632
+ end
633
+
634
+ push_merged_text(open_tag, tag_closed || self_closing || nuke_inner_whitespace ? 0 : 1,
635
+ !nuke_outer_whitespace)
636
+
637
+ @dont_indent_next_line = dont_indent_next_line
638
+ return if tag_closed
639
+ else
640
+ flush_merged_text
641
+ content = value.empty? || parse ? 'nil' : value.dump
642
+ attributes_hash = ', ' + attributes_hash if attributes_hash
643
+ args = [tag_name, self_closing, !block_opened?, preserve_tag, escape_html,
644
+ attributes, nuke_outer_whitespace, nuke_inner_whitespace
645
+ ].map { |v| v.inspect }.join(', ')
646
+ push_silent "_hamlout.open_tag(#{args}, #{object_ref}, #{content}#{attributes_hash})"
647
+ @dont_tab_up_next_text = @dont_indent_next_line = dont_indent_next_line
648
+ end
649
+
650
+ return if self_closing
651
+
652
+ if value.empty?
653
+ push_and_tabulate([:element, [tag_name, nuke_outer_whitespace, nuke_inner_whitespace]])
654
+ @output_tabs += 1 unless nuke_inner_whitespace
655
+ return
656
+ end
657
+
658
+ if parse
659
+ push_script(value, :preserve_script => preserve_script, :in_tag => true,
660
+ :preserve_tag => preserve_tag, :escape_html => escape_html,
661
+ :nuke_inner_whitespace => nuke_inner_whitespace)
662
+ concat_merged_text("</#{tag_name}>" + (nuke_outer_whitespace ? "" : "\n"))
663
+ end
664
+ end
665
+
666
+ # Renders a line that creates an XHTML tag and has an implicit div because of
667
+ # <tt>.</tt> or <tt>#</tt>.
668
+ def render_div(line)
669
+ render_tag('%div' + line)
670
+ end
671
+
672
+ # Renders an XHTML comment.
673
+ def render_comment(line)
674
+ conditional, line = balance(line, ?[, ?]) if line[0] == ?[
675
+ line.strip!
676
+ conditional << ">" if conditional
677
+
678
+ if block_opened? && !line.empty?
679
+ raise SyntaxError.new('Illegal nesting: nesting within a tag that already has content is illegal.', @next_line.index)
680
+ end
681
+
682
+ open = "<!--#{conditional} "
683
+
684
+ # Render it statically if possible
685
+ unless line.empty?
686
+ return push_text("#{open}#{line} #{conditional ? "<![endif]-->" : "-->"}")
687
+ end
688
+
689
+ push_text(open, 1)
690
+ @output_tabs += 1
691
+ push_and_tabulate([:comment, !conditional.nil?])
692
+ unless line.empty?
693
+ push_text(line)
694
+ close
695
+ end
696
+ end
697
+
698
+ # Renders an XHTML doctype or XML shebang.
699
+ def render_doctype(line)
700
+ raise SyntaxError.new("Illegal nesting: nesting within a header command is illegal.", @next_line.index) if block_opened?
701
+ doctype = text_for_doctype(line)
702
+ push_text doctype if doctype
703
+ end
704
+
705
+ def text_for_doctype(text)
706
+ text = text[3..-1].lstrip.downcase
707
+ if text.index("xml") == 0
708
+ return nil if html?
709
+ wrapper = @options[:attr_wrapper]
710
+ return "<?xml version=#{wrapper}1.0#{wrapper} encoding=#{wrapper}#{text.split(' ')[1] || "utf-8"}#{wrapper} ?>"
711
+ end
712
+
713
+ if html5?
714
+ '<!DOCTYPE html>'
715
+ else
716
+ version, type = text.scan(DOCTYPE_REGEX)[0]
717
+
718
+ if xhtml?
719
+ if version == "1.1"
720
+ '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">'
721
+ else
722
+ case type
723
+ when "strict"; '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">'
724
+ when "frameset"; '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">'
725
+ when "mobile"; '<!DOCTYPE html PUBLIC "-//WAPFORUM//DTD XHTML Mobile 1.2//EN" "http://www.openmobilealliance.org/tech/DTD/xhtml-mobile12.dtd">'
726
+ when "basic"; '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML Basic 1.1//EN" "http://www.w3.org/TR/xhtml-basic/xhtml-basic11.dtd">'
727
+ else '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">'
728
+ end
729
+ end
730
+
731
+ elsif html4?
732
+ case type
733
+ when "strict"; '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">'
734
+ when "frameset"; '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Frameset//EN" "http://www.w3.org/TR/html4/frameset.dtd">'
735
+ else '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">'
736
+ end
737
+ end
738
+ end
739
+ end
740
+
741
+ # Starts a filtered block.
742
+ def start_filtered(name)
743
+ raise Error.new("Invalid filter name \":#{name}\".") unless name =~ /^\w+$/
744
+ raise Error.new("Filter \"#{name}\" is not defined.") unless filter = Filters.defined[name]
745
+
746
+ push_and_tabulate([:filtered, filter])
747
+ @flat = true
748
+ @filter_buffer = String.new
749
+
750
+ # If we don't know the indentation by now, it'll be set in Line#tabs
751
+ @flat_spaces = @indentation * @template_tabs if @indentation
752
+ end
753
+
754
+ def raw_next_line
755
+ text = @template.shift
756
+ return unless text
757
+
758
+ index = @template_index
759
+ @template_index += 1
760
+
761
+ return text, index
762
+ end
763
+
764
+ def next_line
765
+ text, index = raw_next_line
766
+ return unless text
767
+
768
+ # :eod is a special end-of-document marker
769
+ line =
770
+ if text == :eod
771
+ Line.new '-#', '-#', '-#', index, self, true
772
+ else
773
+ Line.new text.strip, text.lstrip.chomp, text, index, self, false
774
+ end
775
+
776
+ # `flat?' here is a little outdated,
777
+ # so we have to manually check if either the previous or current line
778
+ # closes the flat block,
779
+ # as well as whether a new block is opened
780
+ @line.tabs if @line
781
+ unless (flat? && !closes_flat?(line) && !closes_flat?(@line)) ||
782
+ (@line && @line.text[0] == ?: && line.full =~ %r[^#{@line.full[/^\s+/]}\s])
783
+ if line.text.empty?
784
+ newline
785
+ return next_line
786
+ end
787
+
788
+ handle_multiline(line)
789
+ end
790
+
791
+ @next_line = line
792
+ end
793
+
794
+ def closes_flat?(line)
795
+ line && !line.text.empty? && line.full !~ /^#{@flat_spaces}/
796
+ end
797
+
798
+ def un_next_line(line)
799
+ @template.unshift line
800
+ @template_index -= 1
801
+ end
802
+
803
+ def handle_multiline(line)
804
+ if is_multiline?(line.text)
805
+ line.text.slice!(-1)
806
+ while new_line = raw_next_line.first
807
+ break if new_line == :eod
808
+ newline and next if new_line.strip.empty?
809
+ break unless is_multiline?(new_line.strip)
810
+ line.text << new_line.strip[0...-1]
811
+ newline
812
+ end
813
+ un_next_line new_line
814
+ resolve_newlines
815
+ end
816
+ end
817
+
818
+ # Checks whether or not +line+ is in a multiline sequence.
819
+ def is_multiline?(text)
820
+ text && text.length > 1 && text[-1] == MULTILINE_CHAR_VALUE && text[-2] == ?\s
821
+ end
822
+
823
+ def contains_interpolation?(str)
824
+ str.include?('#{')
825
+ end
826
+
827
+ def unescape_interpolation(str)
828
+ res = ''
829
+ rest = Haml::Shared.handle_interpolation str.dump do |scan|
830
+ escapes = (scan[2].size - 1) / 2
831
+ res << scan.matched[0...-3 - escapes]
832
+ if escapes % 2 == 1
833
+ res << '#{'
834
+ else
835
+ res << '#{' + eval('"' + balance(scan, ?{, ?}, 1)[0][0...-1] + '"') + "}"# Use eval to get rid of string escapes
836
+ end
837
+ end
838
+ res + rest
839
+ end
840
+
841
+ def balance(*args)
842
+ res = Haml::Shared.balance(*args)
843
+ return res if res
844
+ raise SyntaxError.new("Unbalanced brackets.")
845
+ end
846
+
847
+ def block_opened?
848
+ !flat? && @next_line.tabs > @line.tabs
849
+ end
850
+
851
+ # Pushes value onto <tt>@to_close_stack</tt> and increases
852
+ # <tt>@template_tabs</tt>.
853
+ def push_and_tabulate(value)
854
+ @to_close_stack.push(value)
855
+ @template_tabs += 1
856
+ end
857
+
858
+ def flat?
859
+ @flat
860
+ end
861
+
862
+ def newline
863
+ @newlines += 1
864
+ end
865
+
866
+ def newline_now
867
+ @precompiled << "\n"
868
+ @newlines -= 1
869
+ end
870
+
871
+ def resolve_newlines
872
+ return unless @newlines > 0
873
+ @precompiled << "\n" * @newlines
874
+ @newlines = 0
875
+ end
876
+
877
+ # Get rid of and whitespace at the end of the buffer
878
+ # or the merged text
879
+ def rstrip_buffer!
880
+ if @to_merge.empty?
881
+ push_silent("_erbout.rstrip!", false)
882
+ @dont_tab_up_next_text = true
883
+ return
884
+ end
885
+
886
+ last = @to_merge.last
887
+ case last.first
888
+ when :text
889
+ last[1].rstrip!
890
+ if last[1].empty?
891
+ @to_merge.pop
892
+ rstrip_buffer!
893
+ end
894
+ when :script
895
+ last[1].gsub!(/\(haml_temp, (.*?)\);$/, '(haml_temp.rstrip, \1);')
896
+ else
897
+ raise SyntaxError.new("[HAML BUG] Undefined entry in Haml::Precompiler@to_merge.")
898
+ end
899
+ end
900
+ end
901
+ end