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