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