haml 1.7.2 → 1.8.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of haml might be problematic. Click here for more details.
- data/README +17 -9
- data/Rakefile +12 -4
- data/VERSION +1 -1
- data/init.rb +1 -6
- data/lib/haml.rb +65 -7
- data/lib/haml/buffer.rb +49 -84
- data/lib/haml/engine.rb +155 -797
- data/lib/haml/error.rb +3 -33
- data/lib/haml/exec.rb +86 -65
- data/lib/haml/filters.rb +57 -27
- data/lib/haml/helpers.rb +52 -9
- data/lib/haml/helpers/action_view_mods.rb +1 -1
- data/lib/haml/html.rb +20 -5
- data/lib/haml/precompiler.rb +671 -0
- data/lib/haml/template.rb +20 -73
- data/lib/haml/template/patch.rb +51 -0
- data/lib/haml/template/plugin.rb +21 -0
- data/lib/sass.rb +78 -3
- data/lib/sass/constant.rb +45 -19
- data/lib/sass/constant.rb.rej +42 -0
- data/lib/sass/constant/string.rb +4 -0
- data/lib/sass/css.rb +162 -39
- data/lib/sass/engine.rb +38 -14
- data/lib/sass/plugin.rb +79 -44
- data/lib/sass/tree/attr_node.rb +12 -11
- data/lib/sass/tree/comment_node.rb +9 -3
- data/lib/sass/tree/directive_node.rb +51 -0
- data/lib/sass/tree/node.rb +13 -6
- data/lib/sass/tree/rule_node.rb +34 -12
- data/test/benchmark.rb +85 -52
- data/test/haml/engine_test.rb +172 -84
- data/test/haml/helper_test.rb +31 -3
- data/test/haml/html2haml_test.rb +60 -0
- data/test/haml/markaby/standard.mab +52 -0
- data/test/haml/results/eval_suppressed.xhtml +4 -1
- data/test/haml/results/helpers.xhtml +15 -4
- data/test/haml/results/just_stuff.xhtml +9 -1
- data/test/haml/results/standard.xhtml +0 -1
- data/test/haml/rhtml/_av_partial_1.rhtml +12 -0
- data/test/haml/rhtml/_av_partial_2.rhtml +8 -0
- data/test/haml/rhtml/action_view.rhtml +62 -0
- data/test/haml/rhtml/standard.rhtml +0 -1
- data/test/haml/template_test.rb +41 -21
- data/test/haml/templates/_av_partial_1.haml +9 -0
- data/test/haml/templates/_av_partial_2.haml +5 -0
- data/test/haml/templates/action_view.haml +47 -0
- data/test/haml/templates/eval_suppressed.haml +1 -0
- data/test/haml/templates/helpers.haml +9 -3
- data/test/haml/templates/just_stuff.haml +10 -1
- data/test/haml/templates/partials.haml +1 -1
- data/test/haml/templates/standard.haml +0 -1
- data/test/profile.rb +2 -2
- data/test/sass/engine_test.rb +113 -3
- data/test/sass/engine_test.rb.rej +18 -0
- data/test/sass/plugin_test.rb +34 -11
- data/test/sass/results/compact.css +1 -1
- data/test/sass/results/complex.css +1 -1
- data/test/sass/results/compressed.css +1 -0
- data/test/sass/results/constants.css +3 -1
- data/test/sass/results/expanded.css +2 -1
- data/test/sass/results/import.css +2 -0
- data/test/sass/results/nested.css +2 -1
- data/test/sass/templates/_partial.sass +2 -0
- data/test/sass/templates/compact.sass +2 -0
- data/test/sass/templates/complex.sass +1 -0
- data/test/sass/templates/compressed.sass +15 -0
- data/test/sass/templates/constants.sass +9 -0
- data/test/sass/templates/expanded.sass +2 -0
- data/test/sass/templates/import.sass +1 -1
- data/test/sass/templates/nested.sass +2 -0
- metadata +22 -2
@@ -44,7 +44,7 @@ if defined?(ActionView) and not defined?(Merb::Plugins)
|
|
44
44
|
def form_tag_with_haml(url_for_options = {}, options = {}, *parameters_for_url, &proc)
|
45
45
|
if is_haml?
|
46
46
|
if block_given?
|
47
|
-
oldproc = proc
|
47
|
+
oldproc = proc
|
48
48
|
proc = bind_proc do |*args|
|
49
49
|
concat "\n"
|
50
50
|
tab_up
|
data/lib/haml/html.rb
CHANGED
@@ -27,7 +27,9 @@ module Haml
|
|
27
27
|
match_to_html(template, /<%=(.*?)-?%>/m, 'loud')
|
28
28
|
match_to_html(template, /<%(.*?)-?%>/m, 'silent')
|
29
29
|
end
|
30
|
-
|
30
|
+
|
31
|
+
method = @@options[:xhtml] ? Hpricot.method(:XML) : method(:Hpricot)
|
32
|
+
@template = method.call(template)
|
31
33
|
end
|
32
34
|
end
|
33
35
|
|
@@ -129,7 +131,8 @@ module Haml
|
|
129
131
|
def to_haml(tabs = 0)
|
130
132
|
output = "#{tabulate(tabs)}"
|
131
133
|
if HTML.options[:rhtml] && name[0...5] == 'haml:'
|
132
|
-
return output + HTML.send("haml_tag_#{name[5..-1]}",
|
134
|
+
return output + HTML.send("haml_tag_#{name[5..-1]}",
|
135
|
+
CGI.unescapeHTML(self.innerHTML))
|
133
136
|
end
|
134
137
|
|
135
138
|
output += "%#{name}" unless name == 'div' && (attributes.include?('id') || attributes.include?('class'))
|
@@ -137,9 +140,9 @@ module Haml
|
|
137
140
|
if attributes
|
138
141
|
output += "##{attributes['id']}" if attributes['id']
|
139
142
|
attributes['class'].split(' ').each { |c| output += ".#{c}" } if attributes['class']
|
140
|
-
|
141
|
-
|
142
|
-
output +=
|
143
|
+
remove_attribute('id')
|
144
|
+
remove_attribute('class')
|
145
|
+
output += haml_attributes if attributes.length > 0
|
143
146
|
end
|
144
147
|
|
145
148
|
output += "/" if children.length == 0
|
@@ -151,6 +154,18 @@ module Haml
|
|
151
154
|
|
152
155
|
output
|
153
156
|
end
|
157
|
+
|
158
|
+
private
|
159
|
+
|
160
|
+
# Returns a string representation of an attributes hash
|
161
|
+
# that's prettier than that produced by Hash#inspect
|
162
|
+
def haml_attributes
|
163
|
+
attrs = attributes.map do |name, value|
|
164
|
+
name = name.index(/\W/) ? name.inspect : ":#{name}"
|
165
|
+
"#{name} => #{value.inspect}"
|
166
|
+
end
|
167
|
+
"{ #{attrs.join(', ')} }"
|
168
|
+
end
|
154
169
|
end
|
155
170
|
|
156
171
|
def self.haml_tag_loud(text)
|
@@ -0,0 +1,671 @@
|
|
1
|
+
require 'strscan'
|
2
|
+
|
3
|
+
module Haml
|
4
|
+
module Precompiler
|
5
|
+
# Designates an XHTML/XML element.
|
6
|
+
ELEMENT = ?%
|
7
|
+
|
8
|
+
# Designates a <tt><div></tt> element with the given class.
|
9
|
+
DIV_CLASS = ?.
|
10
|
+
|
11
|
+
# Designates a <tt><div></tt> element with the given id.
|
12
|
+
DIV_ID = ?#
|
13
|
+
|
14
|
+
# Designates an XHTML/XML comment.
|
15
|
+
COMMENT = ?/
|
16
|
+
|
17
|
+
# Designates an XHTML doctype.
|
18
|
+
DOCTYPE = ?!
|
19
|
+
|
20
|
+
# Designates script, the result of which is output.
|
21
|
+
SCRIPT = ?=
|
22
|
+
|
23
|
+
# Designates script, the result of which is flattened and output.
|
24
|
+
FLAT_SCRIPT = ?~
|
25
|
+
|
26
|
+
# Designates script which is run but not output.
|
27
|
+
SILENT_SCRIPT = ?-
|
28
|
+
|
29
|
+
# When following SILENT_SCRIPT, designates a comment that is not output.
|
30
|
+
SILENT_COMMENT = ?#
|
31
|
+
|
32
|
+
# Designates a non-parsed line.
|
33
|
+
ESCAPE = ?\\
|
34
|
+
|
35
|
+
# Designates a block of filtered text.
|
36
|
+
FILTER = ?:
|
37
|
+
|
38
|
+
# Designates a non-parsed line. Not actually a character.
|
39
|
+
PLAIN_TEXT = -1
|
40
|
+
|
41
|
+
# Keeps track of the ASCII values of the characters that begin a
|
42
|
+
# specially-interpreted line.
|
43
|
+
SPECIAL_CHARACTERS = [
|
44
|
+
ELEMENT,
|
45
|
+
DIV_CLASS,
|
46
|
+
DIV_ID,
|
47
|
+
COMMENT,
|
48
|
+
DOCTYPE,
|
49
|
+
SCRIPT,
|
50
|
+
FLAT_SCRIPT,
|
51
|
+
SILENT_SCRIPT,
|
52
|
+
ESCAPE,
|
53
|
+
FILTER
|
54
|
+
]
|
55
|
+
|
56
|
+
# The value of the character that designates that a line is part
|
57
|
+
# of a multiline string.
|
58
|
+
MULTILINE_CHAR_VALUE = ?|
|
59
|
+
|
60
|
+
# Characters that designate that a multiline string may be about
|
61
|
+
# to begin.
|
62
|
+
MULTILINE_STARTERS = SPECIAL_CHARACTERS - [?/]
|
63
|
+
|
64
|
+
# Keywords that appear in the middle of a Ruby block with lowered
|
65
|
+
# indentation. If a block has been started using indentation,
|
66
|
+
# lowering the indentation with one of these won't end the block.
|
67
|
+
# For example:
|
68
|
+
#
|
69
|
+
# - if foo
|
70
|
+
# %p yes!
|
71
|
+
# - else
|
72
|
+
# %p no!
|
73
|
+
#
|
74
|
+
# The block is ended after <tt>%p no!</tt>, because <tt>else</tt>
|
75
|
+
# is a member of this array.
|
76
|
+
MID_BLOCK_KEYWORDS = ['else', 'elsif', 'rescue', 'ensure', 'when']
|
77
|
+
|
78
|
+
# The Regex that matches an HTML comment command.
|
79
|
+
COMMENT_REGEX = /\/(\[[\w\s\.]*\])?(.*)/
|
80
|
+
|
81
|
+
# The Regex that matches a Doctype command.
|
82
|
+
DOCTYPE_REGEX = /(\d\.\d)?[\s]*([a-z]*)/i
|
83
|
+
|
84
|
+
# The Regex that matches an HTML tag command.
|
85
|
+
TAG_REGEX = /[%]([-:\w]+)([-\w\.\#]*)(\{.*\})?(\[.*\])?([=\/\~]?)?(.*)?/
|
86
|
+
|
87
|
+
# The Regex that matches a literal string or symbol value
|
88
|
+
LITERAL_VALUE_REGEX = /^\s*(:(\w*)|(('|")([^\\\#'"]*?)\4))\s*$/
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
# Returns the precompiled string with the preamble and postamble
|
93
|
+
def precompiled_with_ambles(local_names)
|
94
|
+
preamble = <<END.gsub("\n", ";")
|
95
|
+
extend Haml::Helpers
|
96
|
+
@haml_stack ||= Array.new
|
97
|
+
@haml_stack.push(Haml::Buffer.new(#{options_for_buffer.inspect}))
|
98
|
+
@haml_is_haml = true
|
99
|
+
_hamlout = @haml_stack[-1]
|
100
|
+
_erbout = _hamlout.buffer
|
101
|
+
END
|
102
|
+
postamble = <<END.gsub("\n", ";")
|
103
|
+
@haml_is_haml = false
|
104
|
+
@haml_stack.pop.buffer
|
105
|
+
END
|
106
|
+
preamble + locals_code(local_names) + @precompiled + postamble
|
107
|
+
end
|
108
|
+
|
109
|
+
def locals_code(names)
|
110
|
+
names = names.keys if Hash == names
|
111
|
+
|
112
|
+
names.map do |name|
|
113
|
+
"#{name} = _haml_locals[#{name.to_sym.inspect}] || _haml_locals[#{name.to_s.inspect}]"
|
114
|
+
end.join(';') + ';'
|
115
|
+
end
|
116
|
+
|
117
|
+
Line = Struct.new("Line", :text, :unstripped, :index, :spaces, :tabs)
|
118
|
+
|
119
|
+
def precompile
|
120
|
+
@precompiled = ''
|
121
|
+
@merged_text = ''
|
122
|
+
@tab_change = 0
|
123
|
+
|
124
|
+
old_line = Line.new
|
125
|
+
(@template + "\n-#").split("\n").each_with_index do |text, index|
|
126
|
+
line = Line.new text.strip, text.lstrip.chomp, index
|
127
|
+
line.spaces, line.tabs = count_soft_tabs(text)
|
128
|
+
|
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
|
+
suppress_render = handle_multiline(old_line) unless flat?
|
144
|
+
|
145
|
+
if old_line.text.nil? || suppress_render
|
146
|
+
old_line = line
|
147
|
+
newline
|
148
|
+
next
|
149
|
+
end
|
150
|
+
|
151
|
+
process_indent(old_line) unless old_line.text.empty?
|
152
|
+
|
153
|
+
if flat?
|
154
|
+
push_flat(old_line)
|
155
|
+
old_line = line
|
156
|
+
newline
|
157
|
+
next
|
158
|
+
end
|
159
|
+
|
160
|
+
if old_line.spaces != old_line.tabs * 2
|
161
|
+
raise SyntaxError.new("Illegal Indentation: Only two space characters are allowed as tabulation.")
|
162
|
+
end
|
163
|
+
|
164
|
+
unless old_line.text.empty? || @haml_comment
|
165
|
+
process_line(old_line.text, old_line.index, line.tabs > old_line.tabs && !line.text.empty?)
|
166
|
+
end
|
167
|
+
|
168
|
+
if !flat? && line.tabs - old_line.tabs > 1
|
169
|
+
raise SyntaxError.new("Illegal Indentation: Indenting more than once per line is illegal.")
|
170
|
+
end
|
171
|
+
old_line = line
|
172
|
+
newline
|
173
|
+
end
|
174
|
+
|
175
|
+
# Close all the open tags
|
176
|
+
close until @to_close_stack.empty?
|
177
|
+
flush_merged_text
|
178
|
+
end
|
179
|
+
|
180
|
+
# Processes and deals with lowering indentation.
|
181
|
+
def process_indent(line)
|
182
|
+
return unless line.tabs <= @template_tabs && @template_tabs > 0
|
183
|
+
|
184
|
+
to_close = @template_tabs - line.tabs
|
185
|
+
to_close.times { |i| close unless to_close - 1 - i == 0 && mid_block_keyword?(line.text) }
|
186
|
+
end
|
187
|
+
|
188
|
+
# Processes a single line of Haml.
|
189
|
+
#
|
190
|
+
# This method doesn't return anything; it simply processes the line and
|
191
|
+
# adds the appropriate code to <tt>@precompiled</tt>.
|
192
|
+
def process_line(text, index, block_opened)
|
193
|
+
@block_opened = block_opened
|
194
|
+
@index = index + 1
|
195
|
+
|
196
|
+
case text[0]
|
197
|
+
when DIV_CLASS, DIV_ID; render_div(text)
|
198
|
+
when ELEMENT; render_tag(text)
|
199
|
+
when COMMENT; render_comment(text)
|
200
|
+
when SCRIPT
|
201
|
+
return push_script(unescape_interpolation(text[2..-1].strip), false) if text[1] == SCRIPT
|
202
|
+
push_script(text[1..-1], false)
|
203
|
+
when FLAT_SCRIPT; push_flat_script(text[1..-1])
|
204
|
+
when SILENT_SCRIPT
|
205
|
+
return start_haml_comment if text[1] == SILENT_COMMENT
|
206
|
+
|
207
|
+
push_silent(text[1..-1], true)
|
208
|
+
newline true
|
209
|
+
if (@block_opened && !mid_block_keyword?(text)) || text[1..-1].split(' ', 2)[0] == "case"
|
210
|
+
push_and_tabulate([:script])
|
211
|
+
end
|
212
|
+
when FILTER; start_filtered(text[1..-1].downcase)
|
213
|
+
when DOCTYPE
|
214
|
+
return render_doctype(text) if text[0...3] == '!!!'
|
215
|
+
push_plain text
|
216
|
+
when ESCAPE; push_plain text[1..-1]
|
217
|
+
else push_plain text
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
# Returns whether or not the text is a silent script text with one
|
222
|
+
# of Ruby's mid-block keywords.
|
223
|
+
def mid_block_keyword?(text)
|
224
|
+
text.length > 2 && text[0] == SILENT_SCRIPT && MID_BLOCK_KEYWORDS.include?(text[1..-1].split[0])
|
225
|
+
end
|
226
|
+
|
227
|
+
# Deals with all the logic of figuring out whether a given line is
|
228
|
+
# the beginning, continuation, or end of a multiline sequence.
|
229
|
+
#
|
230
|
+
# This returns whether or not the line should be
|
231
|
+
# rendered normally.
|
232
|
+
def handle_multiline(line)
|
233
|
+
text = line.text
|
234
|
+
|
235
|
+
# A multiline string is active, and is being continued
|
236
|
+
if is_multiline?(text) && @multiline
|
237
|
+
@multiline.text << text[0...-1]
|
238
|
+
return true
|
239
|
+
end
|
240
|
+
|
241
|
+
# A multiline string has just been activated, start adding the lines
|
242
|
+
if is_multiline?(text) && (MULTILINE_STARTERS.include? text[0])
|
243
|
+
@multiline = Line.new text[0...-1], nil, line.index, nil, line.tabs
|
244
|
+
process_indent(line)
|
245
|
+
return true
|
246
|
+
end
|
247
|
+
|
248
|
+
# A multiline string has just ended, make line into the result
|
249
|
+
if @multiline && !line.text.empty?
|
250
|
+
process_line(@multiline.text, @multiline.index, line.tabs > @multiline.tabs)
|
251
|
+
@multiline = nil
|
252
|
+
end
|
253
|
+
|
254
|
+
return false
|
255
|
+
end
|
256
|
+
|
257
|
+
# Checks whether or not +line+ is in a multiline sequence.
|
258
|
+
def is_multiline?(text)
|
259
|
+
text && text.length > 1 && text[-1] == MULTILINE_CHAR_VALUE && text[-2] == ?\s
|
260
|
+
end
|
261
|
+
|
262
|
+
# Evaluates <tt>text</tt> in the context of the scope object, but
|
263
|
+
# does not output the result.
|
264
|
+
def push_silent(text, can_suppress = false)
|
265
|
+
flush_merged_text
|
266
|
+
return if can_suppress && options[:suppress_eval]
|
267
|
+
@precompiled << "#{text};"
|
268
|
+
end
|
269
|
+
|
270
|
+
# Adds <tt>text</tt> to <tt>@buffer</tt> with appropriate tabulation
|
271
|
+
# without parsing it.
|
272
|
+
def push_merged_text(text, tab_change = 0, try_one_liner = false)
|
273
|
+
@merged_text << "#{' ' * @output_tabs}#{text}"
|
274
|
+
@tab_change += tab_change
|
275
|
+
@try_one_liner = try_one_liner
|
276
|
+
end
|
277
|
+
|
278
|
+
def push_text(text, tab_change = 0, try_one_liner = false)
|
279
|
+
push_merged_text("#{text}\n", tab_change, try_one_liner)
|
280
|
+
end
|
281
|
+
|
282
|
+
def flush_merged_text
|
283
|
+
return if @merged_text.empty?
|
284
|
+
|
285
|
+
@precompiled << "_hamlout.push_text(#{@merged_text.dump}"
|
286
|
+
@precompiled << ", #{@tab_change}" if @tab_change != 0 || @try_one_liner
|
287
|
+
@precompiled << ");"
|
288
|
+
@merged_text = ''
|
289
|
+
@tab_change = 0
|
290
|
+
@try_one_liner = false
|
291
|
+
end
|
292
|
+
|
293
|
+
# Renders a block of text as plain text.
|
294
|
+
# Also checks for an illegally opened block.
|
295
|
+
def push_plain(text)
|
296
|
+
raise SyntaxError.new("Illegal Nesting: Nesting within plain text is illegal.") if @block_opened
|
297
|
+
push_text text
|
298
|
+
end
|
299
|
+
|
300
|
+
# Adds +text+ to <tt>@buffer</tt> while flattening text.
|
301
|
+
def push_flat(line)
|
302
|
+
tabulation = line.spaces - @flat_spaces
|
303
|
+
tabulation = tabulation > -1 ? tabulation : 0
|
304
|
+
@filter_buffer << "#{' ' * tabulation}#{line.unstripped}\n"
|
305
|
+
end
|
306
|
+
|
307
|
+
# Causes <tt>text</tt> to be evaluated in the context of
|
308
|
+
# the scope object and the result to be added to <tt>@buffer</tt>.
|
309
|
+
#
|
310
|
+
# If <tt>flattened</tt> is true, Haml::Helpers#find_and_flatten is run on
|
311
|
+
# the result before it is added to <tt>@buffer</tt>
|
312
|
+
def push_script(text, flattened, close_tag = nil)
|
313
|
+
flush_merged_text
|
314
|
+
return if options[:suppress_eval]
|
315
|
+
|
316
|
+
push_silent "haml_temp = #{text}"
|
317
|
+
newline true
|
318
|
+
out = "haml_temp = _hamlout.push_script(haml_temp, #{flattened.inspect}, #{close_tag.inspect});"
|
319
|
+
if @block_opened
|
320
|
+
push_and_tabulate([:loud, out])
|
321
|
+
else
|
322
|
+
@precompiled << out
|
323
|
+
end
|
324
|
+
end
|
325
|
+
|
326
|
+
# Causes <tt>text</tt> to be evaluated, and Haml::Helpers#find_and_flatten
|
327
|
+
# to be run on it afterwards.
|
328
|
+
def push_flat_script(text)
|
329
|
+
flush_merged_text
|
330
|
+
|
331
|
+
raise SyntaxError.new("Tag has no content.") if text.empty?
|
332
|
+
push_script(text, true)
|
333
|
+
end
|
334
|
+
|
335
|
+
def start_haml_comment
|
336
|
+
return unless @block_opened
|
337
|
+
|
338
|
+
@haml_comment = true
|
339
|
+
push_and_tabulate([:haml_comment])
|
340
|
+
end
|
341
|
+
|
342
|
+
# Closes the most recent item in <tt>@to_close_stack</tt>.
|
343
|
+
def close
|
344
|
+
tag, value = @to_close_stack.pop
|
345
|
+
case tag
|
346
|
+
when :script; close_block
|
347
|
+
when :comment; close_comment value
|
348
|
+
when :element; close_tag value
|
349
|
+
when :loud; close_loud value
|
350
|
+
when :filtered; close_filtered value
|
351
|
+
when :haml_comment; close_haml_comment
|
352
|
+
end
|
353
|
+
end
|
354
|
+
|
355
|
+
# Puts a line in <tt>@precompiled</tt> that will add the closing tag of
|
356
|
+
# the most recently opened tag.
|
357
|
+
def close_tag(tag)
|
358
|
+
@output_tabs -= 1
|
359
|
+
@template_tabs -= 1
|
360
|
+
push_text("</#{tag}>", -1)
|
361
|
+
end
|
362
|
+
|
363
|
+
# Closes a Ruby block.
|
364
|
+
def close_block
|
365
|
+
push_silent "end", true
|
366
|
+
@template_tabs -= 1
|
367
|
+
end
|
368
|
+
|
369
|
+
# Closes a comment.
|
370
|
+
def close_comment(has_conditional)
|
371
|
+
@output_tabs -= 1
|
372
|
+
@template_tabs -= 1
|
373
|
+
close_tag = has_conditional ? "<![endif]-->" : "-->"
|
374
|
+
push_text(close_tag, -1)
|
375
|
+
end
|
376
|
+
|
377
|
+
# Closes a loud Ruby block.
|
378
|
+
def close_loud(command)
|
379
|
+
push_silent 'end', true
|
380
|
+
@precompiled << command
|
381
|
+
@template_tabs -= 1
|
382
|
+
end
|
383
|
+
|
384
|
+
# Closes a filtered block.
|
385
|
+
def close_filtered(filter)
|
386
|
+
@flat_spaces = -1
|
387
|
+
filtered = filter.new(@filter_buffer).render
|
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
|
+
|
395
|
+
@filter_buffer = nil
|
396
|
+
@template_tabs -= 1
|
397
|
+
end
|
398
|
+
|
399
|
+
def close_haml_comment
|
400
|
+
@haml_comment = false
|
401
|
+
@template_tabs -= 1
|
402
|
+
end
|
403
|
+
|
404
|
+
# Iterates through the classes and ids supplied through <tt>.</tt>
|
405
|
+
# and <tt>#</tt> syntax, and returns a hash with them as attributes,
|
406
|
+
# that can then be merged with another attributes hash.
|
407
|
+
def parse_class_and_id(list)
|
408
|
+
attributes = {}
|
409
|
+
list.scan(/([#.])([-_a-zA-Z0-9]+)/) do |type, property|
|
410
|
+
case type
|
411
|
+
when '.'
|
412
|
+
if attributes['class']
|
413
|
+
attributes['class'] += " "
|
414
|
+
else
|
415
|
+
attributes['class'] = ""
|
416
|
+
end
|
417
|
+
attributes['class'] += property
|
418
|
+
when '#'; attributes['id'] = property
|
419
|
+
end
|
420
|
+
end
|
421
|
+
attributes
|
422
|
+
end
|
423
|
+
|
424
|
+
def parse_literal_value(text)
|
425
|
+
return nil unless text
|
426
|
+
text.match(LITERAL_VALUE_REGEX)
|
427
|
+
|
428
|
+
# $2 holds the value matched by a symbol, but is nil for a string match
|
429
|
+
# $5 holds the value matched by a string
|
430
|
+
$2 || $5
|
431
|
+
end
|
432
|
+
|
433
|
+
def parse_static_hash(text)
|
434
|
+
return {} unless text
|
435
|
+
|
436
|
+
attributes = {}
|
437
|
+
text.split(',').each do |attrib|
|
438
|
+
key, value, more = attrib.split('=>')
|
439
|
+
|
440
|
+
# Make sure the key and value and only the key and value exist
|
441
|
+
# Otherwise, it's too complicated or dynamic and we'll defer it to the actual Ruby parser
|
442
|
+
key = parse_literal_value key
|
443
|
+
value = parse_literal_value value
|
444
|
+
return nil if more || key.nil? || value.nil?
|
445
|
+
|
446
|
+
attributes[key] = value
|
447
|
+
end
|
448
|
+
attributes
|
449
|
+
end
|
450
|
+
|
451
|
+
# This is a class method so it can be accessed from Buffer.
|
452
|
+
def self.build_attributes(attr_wrapper, attributes = {})
|
453
|
+
quote_escape = attr_wrapper == '"' ? """ : "'"
|
454
|
+
other_quote_char = attr_wrapper == '"' ? "'" : '"'
|
455
|
+
|
456
|
+
result = attributes.collect do |attr, value|
|
457
|
+
next if value.nil?
|
458
|
+
|
459
|
+
value = value.to_s
|
460
|
+
this_attr_wrapper = attr_wrapper
|
461
|
+
if value.include? attr_wrapper
|
462
|
+
if value.include? other_quote_char
|
463
|
+
value = value.gsub(attr_wrapper, quote_escape)
|
464
|
+
else
|
465
|
+
this_attr_wrapper = other_quote_char
|
466
|
+
end
|
467
|
+
end
|
468
|
+
" #{attr}=#{this_attr_wrapper}#{value}#{this_attr_wrapper}"
|
469
|
+
end
|
470
|
+
result.compact.sort.join
|
471
|
+
end
|
472
|
+
|
473
|
+
def prerender_tag(name, atomic, attributes)
|
474
|
+
"<#{name}#{Precompiler.build_attributes(@options[:attr_wrapper], attributes)}#{atomic ? ' />' : '>'}"
|
475
|
+
end
|
476
|
+
|
477
|
+
# Parses a line that will render as an XHTML tag, and adds the code that will
|
478
|
+
# render that tag to <tt>@precompiled</tt>.
|
479
|
+
def render_tag(line)
|
480
|
+
raise SyntaxError.new("Invalid tag: \"#{line}\"") unless match = line.scan(TAG_REGEX)[0]
|
481
|
+
tag_name, attributes, attributes_hash, object_ref, action, value = match
|
482
|
+
value = value.to_s.strip
|
483
|
+
attributes_hash = attributes_hash[1...-1] if attributes_hash
|
484
|
+
|
485
|
+
raise SyntaxError.new("Illegal element: classes and ids must have values.") if attributes =~ /[\.#](\.|#|\z)/
|
486
|
+
|
487
|
+
case action
|
488
|
+
when '/'; atomic = true
|
489
|
+
when '~'; parse = flattened = true
|
490
|
+
when '='
|
491
|
+
parse = true
|
492
|
+
value = unescape_interpolation(value[1..-1].strip) if value[0] == ?=
|
493
|
+
end
|
494
|
+
|
495
|
+
if parse && @options[:suppress_eval]
|
496
|
+
parse = false
|
497
|
+
value = ''
|
498
|
+
end
|
499
|
+
|
500
|
+
object_ref = "nil" if object_ref.nil? || @options[:suppress_eval]
|
501
|
+
|
502
|
+
static_attributes = parse_static_hash(attributes_hash) # Try pre-compiling a static attributes hash
|
503
|
+
attributes_hash = nil if static_attributes || @options[:suppress_eval]
|
504
|
+
attributes = parse_class_and_id(attributes)
|
505
|
+
Buffer.merge_attrs(attributes, static_attributes) if static_attributes
|
506
|
+
|
507
|
+
raise SyntaxError.new("Illegal Nesting: Nesting within an atomic tag is illegal.") if @block_opened && atomic
|
508
|
+
raise SyntaxError.new("Illegal Nesting: Nesting within a tag that already has content is illegal.") if @block_opened && !value.empty?
|
509
|
+
raise SyntaxError.new("Tag has no content.") if parse && value.empty?
|
510
|
+
raise SyntaxError.new("Atomic tags can't have content.") if atomic && !value.empty?
|
511
|
+
|
512
|
+
atomic = true if !@block_opened && value.empty? && @options[:autoclose].include?(tag_name)
|
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
|
517
|
+
|
518
|
+
open_tag = prerender_tag(tag_name, atomic, attributes)
|
519
|
+
open_tag << "#{value}</#{tag_name}>" if tag_closed
|
520
|
+
open_tag << "\n" unless parse
|
521
|
+
|
522
|
+
push_merged_text(open_tag, tag_closed || atomic ? 0 : 1, parse)
|
523
|
+
return if tag_closed
|
524
|
+
else
|
525
|
+
flush_merged_text
|
526
|
+
content = value.empty? || parse ? 'nil' : value.dump
|
527
|
+
attributes_hash = ', ' + attributes_hash if attributes_hash
|
528
|
+
push_silent "_hamlout.open_tag(#{tag_name.inspect}, #{atomic.inspect}, #{(!value.empty?).inspect}, #{attributes.inspect}, #{object_ref}, #{content}#{attributes_hash})"
|
529
|
+
end
|
530
|
+
|
531
|
+
return if atomic
|
532
|
+
|
533
|
+
if value.empty?
|
534
|
+
push_and_tabulate([:element, tag_name])
|
535
|
+
@output_tabs += 1
|
536
|
+
return
|
537
|
+
end
|
538
|
+
|
539
|
+
if parse
|
540
|
+
flush_merged_text
|
541
|
+
push_script(value, flattened, tag_name)
|
542
|
+
end
|
543
|
+
end
|
544
|
+
|
545
|
+
# Renders a line that creates an XHTML tag and has an implicit div because of
|
546
|
+
# <tt>.</tt> or <tt>#</tt>.
|
547
|
+
def render_div(line)
|
548
|
+
render_tag('%div' + line)
|
549
|
+
end
|
550
|
+
|
551
|
+
# Renders an XHTML comment.
|
552
|
+
def render_comment(line)
|
553
|
+
conditional, content = line.scan(COMMENT_REGEX)[0]
|
554
|
+
content.strip!
|
555
|
+
conditional << ">" if conditional
|
556
|
+
|
557
|
+
if @block_opened && !content.empty?
|
558
|
+
raise SyntaxError.new('Illegal Nesting: Nesting within a tag that already has content is illegal.')
|
559
|
+
end
|
560
|
+
|
561
|
+
open = "<!--#{conditional} "
|
562
|
+
|
563
|
+
# Render it statically if possible
|
564
|
+
if !content.empty? && Buffer.one_liner?(content)
|
565
|
+
return push_text("#{open}#{content} #{conditional ? "<![endif]-->" : "-->"}")
|
566
|
+
end
|
567
|
+
|
568
|
+
push_text(open, 1)
|
569
|
+
@output_tabs += 1
|
570
|
+
push_and_tabulate([:comment, !conditional.nil?])
|
571
|
+
unless content.empty?
|
572
|
+
push_text(content)
|
573
|
+
close
|
574
|
+
end
|
575
|
+
end
|
576
|
+
|
577
|
+
# Renders an XHTML doctype or XML shebang.
|
578
|
+
def render_doctype(line)
|
579
|
+
raise SyntaxError.new("Illegal Nesting: Nesting within a header command is illegal.") if @block_opened
|
580
|
+
push_text text_for_doctype(line)
|
581
|
+
end
|
582
|
+
|
583
|
+
def text_for_doctype(text)
|
584
|
+
text = text[3..-1].lstrip.downcase
|
585
|
+
if text[0...3] == "xml"
|
586
|
+
wrapper = @options[:attr_wrapper]
|
587
|
+
return "<?xml version=#{wrapper}1.0#{wrapper} encoding=#{wrapper}#{text.split(' ')[1] || "utf-8"}#{wrapper} ?>"
|
588
|
+
end
|
589
|
+
|
590
|
+
version, type = text.scan(DOCTYPE_REGEX)[0]
|
591
|
+
if version == "1.1"
|
592
|
+
return '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">'
|
593
|
+
end
|
594
|
+
|
595
|
+
case type
|
596
|
+
when "strict"; return '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">'
|
597
|
+
when "frameset"; return '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">'
|
598
|
+
else return '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">'
|
599
|
+
end
|
600
|
+
end
|
601
|
+
|
602
|
+
# Starts a filtered block.
|
603
|
+
def start_filtered(name)
|
604
|
+
raise SyntaxError.new('Filters must have nested text.') unless @block_opened
|
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
|
612
|
+
|
613
|
+
push_and_tabulate([:filtered, filter])
|
614
|
+
@flat_spaces = @template_tabs * 2
|
615
|
+
@filter_buffer = String.new
|
616
|
+
end
|
617
|
+
|
618
|
+
def unescape_interpolation(str)
|
619
|
+
scan = StringScanner.new(str.dump)
|
620
|
+
str = ''
|
621
|
+
|
622
|
+
while scan.scan(/(.*?)\\\#\{/)
|
623
|
+
str << scan.matched[0...-3]
|
624
|
+
str << eval("\"\\\#{#{balance_brackets(scan)}}\"")
|
625
|
+
end
|
626
|
+
|
627
|
+
str + scan.rest
|
628
|
+
end
|
629
|
+
|
630
|
+
def balance_brackets(scanner)
|
631
|
+
str = ''
|
632
|
+
count = 1
|
633
|
+
|
634
|
+
while scanner.scan(/(.*?)[\{\}]/)
|
635
|
+
str << scanner.matched
|
636
|
+
count += 1 if scanner.matched[-1] == ?{
|
637
|
+
count -= 1 if scanner.matched[-1] == ?}
|
638
|
+
return str[0...-1] if count == 0
|
639
|
+
end
|
640
|
+
|
641
|
+
raise SyntaxError.new("Unbalanced brackets.")
|
642
|
+
end
|
643
|
+
|
644
|
+
# Counts the tabulation of a line.
|
645
|
+
def count_soft_tabs(line)
|
646
|
+
spaces = line.index(/([^ ]|$)/)
|
647
|
+
if line[spaces] == ?\t
|
648
|
+
return nil if line.strip.empty?
|
649
|
+
raise SyntaxError.new("Illegal Indentation: Only two space characters are allowed as tabulation.")
|
650
|
+
end
|
651
|
+
[spaces, spaces/2]
|
652
|
+
end
|
653
|
+
|
654
|
+
# Pushes value onto <tt>@to_close_stack</tt> and increases
|
655
|
+
# <tt>@template_tabs</tt>.
|
656
|
+
def push_and_tabulate(value)
|
657
|
+
@to_close_stack.push(value)
|
658
|
+
@template_tabs += 1
|
659
|
+
end
|
660
|
+
|
661
|
+
def flat?
|
662
|
+
@flat_spaces != -1
|
663
|
+
end
|
664
|
+
|
665
|
+
def newline(skip_next = false)
|
666
|
+
return @skip_next_newline = false if @skip_next_newline
|
667
|
+
@skip_next_newline = true if skip_next
|
668
|
+
@precompiled << "\n"
|
669
|
+
end
|
670
|
+
end
|
671
|
+
end
|