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