haml-edge 2.1.1

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