haml 3.1.0.alpha.26 → 3.1.0.alpha.27
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/EDGE_GEM_VERSION +1 -1
- data/REVISION +1 -0
- data/VERSION +1 -1
- data/lib/haml/buffer.rb +3 -3
- data/lib/haml/compiler.rb +432 -0
- data/lib/haml/engine.rb +5 -3
- data/lib/haml/filters.rb +20 -18
- data/lib/haml/helpers.rb +5 -5
- data/lib/haml/parser.rb +706 -0
- data/lib/haml/template.rb +1 -1
- data/lib/haml/template/plugin.rb +10 -9
- data/test/haml/engine_test.rb +34 -1
- data/vendor/sass/lib/sass/version.rb +1 -0
- metadata +5 -3
- data/lib/haml/precompiler.rb +0 -1113
data/lib/haml/engine.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
require 'haml/helpers'
|
2
2
|
require 'haml/buffer'
|
3
|
-
require 'haml/
|
3
|
+
require 'haml/parser'
|
4
|
+
require 'haml/compiler'
|
4
5
|
require 'haml/filters'
|
5
6
|
require 'haml/error'
|
6
7
|
|
@@ -15,7 +16,8 @@ module Haml
|
|
15
16
|
# output = haml_engine.render
|
16
17
|
# puts output
|
17
18
|
class Engine
|
18
|
-
include
|
19
|
+
include Parser
|
20
|
+
include Compiler
|
19
21
|
|
20
22
|
# The options hash.
|
21
23
|
# See {file:HAML_REFERENCE.md#haml_options the Haml options documentation}.
|
@@ -119,7 +121,7 @@ module Haml
|
|
119
121
|
@to_merge = []
|
120
122
|
@tab_change = 0
|
121
123
|
|
122
|
-
|
124
|
+
compile(parse)
|
123
125
|
rescue Haml::Error => e
|
124
126
|
if @index || e.line
|
125
127
|
e.backtrace.unshift "#{@options[:filename]}:#{(e.line ? e.line + 1 : @index) + @options[:line] - 1}"
|
data/lib/haml/filters.rb
CHANGED
@@ -82,21 +82,21 @@ module Haml
|
|
82
82
|
|
83
83
|
# This should be overridden when a filter needs to have access to the Haml evaluation context.
|
84
84
|
# Rather than applying a filter to a string at compile-time,
|
85
|
-
# \{#compile} uses the {Haml::
|
85
|
+
# \{#compile} uses the {Haml::Compiler} instance to compile the string to Ruby code
|
86
86
|
# that will be executed in the context of the active Haml template.
|
87
87
|
#
|
88
|
-
# Warning: the {Haml::
|
88
|
+
# Warning: the {Haml::Compiler} interface is neither well-documented
|
89
89
|
# nor guaranteed to be stable.
|
90
90
|
# If you want to make use of it, you'll probably need to look at the source code
|
91
91
|
# and should test your filter when upgrading to new Haml versions.
|
92
92
|
#
|
93
|
-
# @param
|
93
|
+
# @param compiler [Haml::Compiler] The compiler instance
|
94
94
|
# @param text [String] The text of the filter
|
95
95
|
# @raise [Haml::Error] if none of \{#compile}, \{#render}, and \{#render_with_options} are overridden
|
96
|
-
def compile(
|
96
|
+
def compile(compiler, text)
|
97
97
|
resolve_lazy_requires
|
98
98
|
filter = self
|
99
|
-
|
99
|
+
compiler.instance_eval do
|
100
100
|
if contains_interpolation?(text)
|
101
101
|
return if options[:suppress_eval]
|
102
102
|
|
@@ -105,24 +105,26 @@ module Haml
|
|
105
105
|
next s if escapes % 2 == 0
|
106
106
|
("\\" * (escapes - 1)) + "\n"
|
107
107
|
end
|
108
|
-
newline
|
109
|
-
|
108
|
+
# We need to add a newline at the beginning to get the
|
109
|
+
# filter lines to line up (since the Haml filter contains
|
110
|
+
# a line that doesn't show up in the source, namely the
|
111
|
+
# filter name). Then we need to escape the trailing
|
112
|
+
# newline so that the whole filter block doesn't take up
|
113
|
+
# too many.
|
114
|
+
text = "\n" + text.sub(/\n"\Z/, "\\n\"")
|
115
|
+
push_script <<RUBY.rstrip, :escape_html => false
|
110
116
|
find_and_preserve(#{filter.inspect}.render_with_options(#{text}, _hamlout.options))
|
111
117
|
RUBY
|
112
118
|
return
|
113
119
|
end
|
114
120
|
|
115
|
-
rendered = Haml::Helpers::find_and_preserve(filter.render_with_options(text,
|
121
|
+
rendered = Haml::Helpers::find_and_preserve(filter.render_with_options(text, compiler.options), compiler.options[:preserve])
|
116
122
|
|
117
123
|
if !options[:ugly]
|
118
124
|
push_text(rendered.rstrip.gsub("\n", "\n#{' ' * @output_tabs}"))
|
119
125
|
else
|
120
126
|
push_text(rendered.rstrip)
|
121
127
|
end
|
122
|
-
|
123
|
-
(text.count("\n") - 1).times {newline}
|
124
|
-
resolve_newlines
|
125
|
-
newline
|
126
128
|
end
|
127
129
|
end
|
128
130
|
|
@@ -270,9 +272,9 @@ END
|
|
270
272
|
lazy_require 'stringio'
|
271
273
|
|
272
274
|
# @see Base#compile
|
273
|
-
def compile(
|
274
|
-
return if
|
275
|
-
|
275
|
+
def compile(compiler, text)
|
276
|
+
return if compiler.options[:suppress_eval]
|
277
|
+
compiler.instance_eval do
|
276
278
|
push_silent <<-FIRST.gsub("\n", ';') + text + <<-LAST.gsub("\n", ';')
|
277
279
|
_haml_old_stdout = $stdout
|
278
280
|
$stdout = StringIO.new(_hamlout.buffer, 'a')
|
@@ -318,11 +320,11 @@ END
|
|
318
320
|
lazy_require 'erb'
|
319
321
|
|
320
322
|
# @see Base#compile
|
321
|
-
def compile(
|
322
|
-
return if
|
323
|
+
def compile(compiler, text)
|
324
|
+
return if compiler.options[:suppress_eval]
|
323
325
|
src = ::ERB.new(text).src.sub(/^#coding:.*?\n/, '').
|
324
326
|
sub(/^_erbout = '';/, "")
|
325
|
-
|
327
|
+
compiler.send(:push_silent, src)
|
326
328
|
end
|
327
329
|
end
|
328
330
|
|
data/lib/haml/helpers.rb
CHANGED
@@ -445,10 +445,10 @@ MESSAGE
|
|
445
445
|
attrs = Haml::Util.map_keys(rest.shift || {}) {|key| key.to_s}
|
446
446
|
name, attrs = merge_name_and_attributes(name.to_s, attrs)
|
447
447
|
|
448
|
-
attributes = Haml::
|
449
|
-
|
450
|
-
|
451
|
-
|
448
|
+
attributes = Haml::Compiler.build_attributes(haml_buffer.html?,
|
449
|
+
haml_buffer.options[:attr_wrapper],
|
450
|
+
haml_buffer.options[:escape_attrs],
|
451
|
+
attrs)
|
452
452
|
|
453
453
|
if text.nil? && block.nil? && (haml_buffer.options[:autoclose].include?(name) || flags.include?(:/))
|
454
454
|
haml_concat "<#{name}#{attributes} />"
|
@@ -553,7 +553,7 @@ MESSAGE
|
|
553
553
|
return name, attributes_hash unless name =~ /^(.+?)?([\.#].*)$/
|
554
554
|
|
555
555
|
return $1 || "div", Buffer.merge_attrs(
|
556
|
-
|
556
|
+
Haml::Parser.parse_class_and_id($2), attributes_hash)
|
557
557
|
end
|
558
558
|
|
559
559
|
# Runs a block of code with the given buffer as the currently active buffer.
|
data/lib/haml/parser.rb
ADDED
@@ -0,0 +1,706 @@
|
|
1
|
+
require 'strscan'
|
2
|
+
require 'haml/shared'
|
3
|
+
|
4
|
+
module Haml
|
5
|
+
module Parser
|
6
|
+
include Haml::Util
|
7
|
+
|
8
|
+
# Designates an XHTML/XML element.
|
9
|
+
ELEMENT = ?%
|
10
|
+
|
11
|
+
# Designates a `<div>` element with the given class.
|
12
|
+
DIV_CLASS = ?.
|
13
|
+
|
14
|
+
# Designates a `<div>` element with the given id.
|
15
|
+
DIV_ID = ?#
|
16
|
+
|
17
|
+
# Designates an XHTML/XML comment.
|
18
|
+
COMMENT = ?/
|
19
|
+
|
20
|
+
# Designates an XHTML doctype or script that is never HTML-escaped.
|
21
|
+
DOCTYPE = ?!
|
22
|
+
|
23
|
+
# Designates script, the result of which is output.
|
24
|
+
SCRIPT = ?=
|
25
|
+
|
26
|
+
# Designates script that is always HTML-escaped.
|
27
|
+
SANITIZE = ?&
|
28
|
+
|
29
|
+
# Designates script, the result of which is flattened and output.
|
30
|
+
FLAT_SCRIPT = ?~
|
31
|
+
|
32
|
+
# Designates script which is run but not output.
|
33
|
+
SILENT_SCRIPT = ?-
|
34
|
+
|
35
|
+
# When following SILENT_SCRIPT, designates a comment that is not output.
|
36
|
+
SILENT_COMMENT = ?#
|
37
|
+
|
38
|
+
# Designates a non-parsed line.
|
39
|
+
ESCAPE = ?\\
|
40
|
+
|
41
|
+
# Designates a block of filtered text.
|
42
|
+
FILTER = ?:
|
43
|
+
|
44
|
+
# Designates a non-parsed line. Not actually a character.
|
45
|
+
PLAIN_TEXT = -1
|
46
|
+
|
47
|
+
# Keeps track of the ASCII values of the characters that begin a
|
48
|
+
# specially-interpreted line.
|
49
|
+
SPECIAL_CHARACTERS = [
|
50
|
+
ELEMENT,
|
51
|
+
DIV_CLASS,
|
52
|
+
DIV_ID,
|
53
|
+
COMMENT,
|
54
|
+
DOCTYPE,
|
55
|
+
SCRIPT,
|
56
|
+
SANITIZE,
|
57
|
+
FLAT_SCRIPT,
|
58
|
+
SILENT_SCRIPT,
|
59
|
+
ESCAPE,
|
60
|
+
FILTER
|
61
|
+
]
|
62
|
+
|
63
|
+
# The value of the character that designates that a line is part
|
64
|
+
# of a multiline string.
|
65
|
+
MULTILINE_CHAR_VALUE = ?|
|
66
|
+
|
67
|
+
MID_BLOCK_KEYWORDS = %w[else elsif rescue ensure end when]
|
68
|
+
START_BLOCK_KEYWORDS = %w[if begin case]
|
69
|
+
# Try to parse assignments to block starters as best as possible
|
70
|
+
START_BLOCK_KEYWORD_REGEX = /(?:\w+(?:,\s*\w+)*\s*=\s*)?(#{START_BLOCK_KEYWORDS.join('|')})/
|
71
|
+
BLOCK_KEYWORD_REGEX = /^-\s*(?:(#{MID_BLOCK_KEYWORDS.join('|')})|#{START_BLOCK_KEYWORD_REGEX.source})\b/
|
72
|
+
|
73
|
+
# The Regex that matches a Doctype command.
|
74
|
+
DOCTYPE_REGEX = /(\d(?:\.\d)?)?[\s]*([a-z]*)\s*([^ ]+)?/i
|
75
|
+
|
76
|
+
# The Regex that matches a literal string or symbol value
|
77
|
+
LITERAL_VALUE_REGEX = /:(\w*)|(["'])((?![\\#]|\2).|\\.)*\2/
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
# @private
|
82
|
+
class Line < Struct.new(:text, :unstripped, :full, :index, :compiler, :eod)
|
83
|
+
alias_method :eod?, :eod
|
84
|
+
|
85
|
+
# @private
|
86
|
+
def tabs
|
87
|
+
line = self
|
88
|
+
@tabs ||= compiler.instance_eval do
|
89
|
+
break 0 if line.text.empty? || !(whitespace = line.full[/^\s+/])
|
90
|
+
|
91
|
+
if @indentation.nil?
|
92
|
+
@indentation = whitespace
|
93
|
+
|
94
|
+
if @indentation.include?(?\s) && @indentation.include?(?\t)
|
95
|
+
raise SyntaxError.new("Indentation can't use both tabs and spaces.", line.index)
|
96
|
+
end
|
97
|
+
|
98
|
+
@flat_spaces = @indentation * (@template_tabs+1) if flat?
|
99
|
+
break 1
|
100
|
+
end
|
101
|
+
|
102
|
+
tabs = whitespace.length / @indentation.length
|
103
|
+
break tabs if whitespace == @indentation * tabs
|
104
|
+
break @template_tabs + 1 if flat? && whitespace =~ /^#{@flat_spaces}/
|
105
|
+
|
106
|
+
raise SyntaxError.new(<<END.strip.gsub("\n", ' '), line.index)
|
107
|
+
Inconsistent indentation: #{Haml::Shared.human_indentation whitespace, true} used for indentation,
|
108
|
+
but the rest of the document was indented using #{Haml::Shared.human_indentation @indentation}.
|
109
|
+
END
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
# @private
|
115
|
+
class ParseNode < Struct.new(:type, :line, :value, :parent, :children)
|
116
|
+
def initialize(*args)
|
117
|
+
super
|
118
|
+
self.children ||= []
|
119
|
+
end
|
120
|
+
|
121
|
+
def inspect
|
122
|
+
text = "(#{type} #{value.inspect}"
|
123
|
+
children.each {|c| text << "\n" << c.inspect.gsub(/^/, " ")}
|
124
|
+
text + ")"
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def parse
|
129
|
+
@root = @parent = ParseNode.new(:root)
|
130
|
+
@haml_comment = false
|
131
|
+
@indentation = nil
|
132
|
+
@line = next_line
|
133
|
+
|
134
|
+
raise SyntaxError.new("Indenting at the beginning of the document is illegal.", @line.index) if @line.tabs != 0
|
135
|
+
|
136
|
+
while next_line
|
137
|
+
process_indent(@line) unless @line.text.empty?
|
138
|
+
|
139
|
+
if flat?
|
140
|
+
text = @line.full.dup
|
141
|
+
text = "" unless text.gsub!(/^#{@flat_spaces}/, '')
|
142
|
+
@filter_buffer << "#{text}\n"
|
143
|
+
@line = @next_line
|
144
|
+
next
|
145
|
+
end
|
146
|
+
|
147
|
+
@tab_up = nil
|
148
|
+
process_line(@line.text, @line.index) unless @line.text.empty? || @haml_comment
|
149
|
+
if block_opened? || @tab_up
|
150
|
+
@template_tabs += 1
|
151
|
+
@parent = @parent.children.last
|
152
|
+
end
|
153
|
+
|
154
|
+
if !flat? && @next_line.tabs - @line.tabs > 1
|
155
|
+
raise SyntaxError.new("The line was indented #{@next_line.tabs - @line.tabs} levels deeper than the previous line.", @next_line.index)
|
156
|
+
end
|
157
|
+
|
158
|
+
@line = @next_line
|
159
|
+
end
|
160
|
+
|
161
|
+
# Close all the open tags
|
162
|
+
close until @parent.type == :root
|
163
|
+
@root
|
164
|
+
end
|
165
|
+
|
166
|
+
# Processes and deals with lowering indentation.
|
167
|
+
def process_indent(line)
|
168
|
+
return unless line.tabs <= @template_tabs && @template_tabs > 0
|
169
|
+
|
170
|
+
to_close = @template_tabs - line.tabs
|
171
|
+
to_close.times {|i| close unless to_close - 1 - i == 0 && mid_block_keyword?(line.text)}
|
172
|
+
end
|
173
|
+
|
174
|
+
# Processes a single line of Haml.
|
175
|
+
#
|
176
|
+
# This method doesn't return anything; it simply processes the line and
|
177
|
+
# adds the appropriate code to `@precompiled`.
|
178
|
+
def process_line(text, index)
|
179
|
+
@index = index + 1
|
180
|
+
|
181
|
+
case text[0]
|
182
|
+
when DIV_CLASS; push div(text)
|
183
|
+
when DIV_ID
|
184
|
+
return push plain(text) if text[1] == ?{
|
185
|
+
push div(text)
|
186
|
+
when ELEMENT; push tag(text)
|
187
|
+
when COMMENT; push comment(text[1..-1].strip)
|
188
|
+
when SANITIZE
|
189
|
+
return push plain(text[3..-1].strip, :escape_html) if text[1..2] == "=="
|
190
|
+
return push script(text[2..-1].strip, :escape_html) if text[1] == SCRIPT
|
191
|
+
return push flat_script(text[2..-1].strip, :escape_html) if text[1] == FLAT_SCRIPT
|
192
|
+
return push plain(text[1..-1].strip, :escape_html) if text[1] == ?\s
|
193
|
+
push plain(text)
|
194
|
+
when SCRIPT
|
195
|
+
return push plain(text[2..-1].strip) if text[1] == SCRIPT
|
196
|
+
push script(text[1..-1])
|
197
|
+
when FLAT_SCRIPT; push flat_script(text[1..-1])
|
198
|
+
when SILENT_SCRIPT; push silent_script(text)
|
199
|
+
when FILTER; push filter(text[1..-1].downcase)
|
200
|
+
when DOCTYPE
|
201
|
+
return push doctype(text) if text[0...3] == '!!!'
|
202
|
+
return push plain(text[3..-1].strip, !:escape_html) if text[1..2] == "=="
|
203
|
+
return push script(text[2..-1].strip, !:escape_html) if text[1] == SCRIPT
|
204
|
+
return push flat_script(text[2..-1].strip, !:escape_html) if text[1] == FLAT_SCRIPT
|
205
|
+
return push plain(text[1..-1].strip, !:escape_html) if text[1] == ?\s
|
206
|
+
push plain(text)
|
207
|
+
when ESCAPE; push plain(text[1..-1])
|
208
|
+
else; push plain(text)
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
def block_keyword(text)
|
213
|
+
return unless keyword = text.scan(BLOCK_KEYWORD_REGEX)[0]
|
214
|
+
keyword[0] || keyword[1]
|
215
|
+
end
|
216
|
+
|
217
|
+
def mid_block_keyword?(text)
|
218
|
+
MID_BLOCK_KEYWORDS.include?(block_keyword(text))
|
219
|
+
end
|
220
|
+
|
221
|
+
def push(node)
|
222
|
+
@parent.children << node
|
223
|
+
node.parent = @parent
|
224
|
+
end
|
225
|
+
|
226
|
+
def plain(text, escape_html = nil)
|
227
|
+
if block_opened?
|
228
|
+
raise SyntaxError.new("Illegal nesting: nesting within plain text is illegal.", @next_line.index)
|
229
|
+
end
|
230
|
+
|
231
|
+
unless contains_interpolation?(text)
|
232
|
+
return ParseNode.new(:plain, @index, :text => text)
|
233
|
+
end
|
234
|
+
|
235
|
+
escape_html = @options[:escape_html] if escape_html.nil?
|
236
|
+
script(unescape_interpolation(text, escape_html), !:escape_html)
|
237
|
+
end
|
238
|
+
|
239
|
+
def script(text, escape_html = nil, preserve = false)
|
240
|
+
raise SyntaxError.new("There's no Ruby code for = to evaluate.") if text.empty?
|
241
|
+
text = handle_ruby_multiline(text)
|
242
|
+
escape_html = @options[:escape_html] if escape_html.nil?
|
243
|
+
|
244
|
+
ParseNode.new(:script, @index, :text => text, :escape_html => escape_html,
|
245
|
+
:preserve => preserve)
|
246
|
+
end
|
247
|
+
|
248
|
+
def flat_script(text, escape_html = nil)
|
249
|
+
raise SyntaxError.new("There's no Ruby code for ~ to evaluate.") if text.empty?
|
250
|
+
script(text, escape_html, :preserve)
|
251
|
+
end
|
252
|
+
|
253
|
+
def silent_script(text)
|
254
|
+
return haml_comment(text[2..-1]) if text[1] == SILENT_COMMENT
|
255
|
+
|
256
|
+
raise SyntaxError.new(<<END.rstrip, @index - 1) if text[1..-1].strip == "end"
|
257
|
+
You don't need to use "- end" in Haml. Un-indent to close a block:
|
258
|
+
- if foo?
|
259
|
+
%strong Foo!
|
260
|
+
- else
|
261
|
+
Not foo.
|
262
|
+
%p This line is un-indented, so it isn't part of the "if" block
|
263
|
+
END
|
264
|
+
|
265
|
+
text = handle_ruby_multiline(text)
|
266
|
+
keyword = block_keyword(text)
|
267
|
+
|
268
|
+
@tab_up = ["if", "case"].include?(keyword)
|
269
|
+
ParseNode.new(:silent_script, @index,
|
270
|
+
:text => text[1..-1], :keyword => keyword)
|
271
|
+
end
|
272
|
+
|
273
|
+
def haml_comment(text)
|
274
|
+
@haml_comment = block_opened?
|
275
|
+
ParseNode.new(:haml_comment, @index, :text => text)
|
276
|
+
end
|
277
|
+
|
278
|
+
def tag(line)
|
279
|
+
tag_name, attributes, attributes_hashes, object_ref, nuke_outer_whitespace,
|
280
|
+
nuke_inner_whitespace, action, value, last_line = parse_tag(line)
|
281
|
+
|
282
|
+
preserve_tag = @options[:preserve].include?(tag_name)
|
283
|
+
nuke_inner_whitespace ||= preserve_tag
|
284
|
+
preserve_tag = false if @options[:ugly]
|
285
|
+
escape_html = (action == '&' || (action != '!' && @options[:escape_html]))
|
286
|
+
|
287
|
+
case action
|
288
|
+
when '/'; self_closing = true
|
289
|
+
when '~'; parse = preserve_script = true
|
290
|
+
when '='
|
291
|
+
parse = true
|
292
|
+
if value[0] == ?=
|
293
|
+
value = unescape_interpolation(value[1..-1].strip, escape_html)
|
294
|
+
escape_html = false
|
295
|
+
end
|
296
|
+
when '&', '!'
|
297
|
+
if value[0] == ?= || value[0] == ?~
|
298
|
+
parse = true
|
299
|
+
preserve_script = (value[0] == ?~)
|
300
|
+
if value[1] == ?=
|
301
|
+
value = unescape_interpolation(value[2..-1].strip, escape_html)
|
302
|
+
escape_html = false
|
303
|
+
else
|
304
|
+
value = value[1..-1].strip
|
305
|
+
end
|
306
|
+
elsif contains_interpolation?(value)
|
307
|
+
value = unescape_interpolation(value, escape_html)
|
308
|
+
parse = true
|
309
|
+
escape_html = false
|
310
|
+
end
|
311
|
+
else
|
312
|
+
if contains_interpolation?(value)
|
313
|
+
value = unescape_interpolation(value, escape_html)
|
314
|
+
parse = true
|
315
|
+
escape_html = false
|
316
|
+
end
|
317
|
+
end
|
318
|
+
|
319
|
+
attributes = Parser.parse_class_and_id(attributes)
|
320
|
+
attributes_hashes.map! do |syntax, attributes_hash|
|
321
|
+
if syntax == :old
|
322
|
+
static_attributes = parse_static_hash(attributes_hash)
|
323
|
+
attributes_hash = nil if static_attributes
|
324
|
+
else
|
325
|
+
static_attributes, attributes_hash = attributes_hash
|
326
|
+
end
|
327
|
+
Buffer.merge_attrs(attributes, static_attributes) if static_attributes
|
328
|
+
attributes_hash
|
329
|
+
end.compact!
|
330
|
+
|
331
|
+
raise SyntaxError.new("Illegal nesting: nesting within a self-closing tag is illegal.", @next_line.index) if block_opened? && self_closing
|
332
|
+
raise SyntaxError.new("There's no Ruby code for #{action} to evaluate.", last_line - 1) if parse && value.empty?
|
333
|
+
raise SyntaxError.new("Self-closing tags can't have content.", last_line - 1) if self_closing && !value.empty?
|
334
|
+
|
335
|
+
if block_opened? && !value.empty? && !is_ruby_multiline?(value)
|
336
|
+
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)
|
337
|
+
end
|
338
|
+
|
339
|
+
self_closing ||= !!(!block_opened? && value.empty? && @options[:autoclose].any? {|t| t === tag_name})
|
340
|
+
value = nil if value.empty? && (block_opened? || self_closing)
|
341
|
+
value = handle_ruby_multiline(value) if parse
|
342
|
+
|
343
|
+
ParseNode.new(:tag, @index, :name => tag_name, :attributes => attributes,
|
344
|
+
:attributes_hashes => attributes_hashes, :self_closing => self_closing,
|
345
|
+
:nuke_inner_whitespace => nuke_inner_whitespace,
|
346
|
+
:nuke_outer_whitespace => nuke_outer_whitespace, :object_ref => object_ref,
|
347
|
+
:escape_html => escape_html, :preserve_tag => preserve_tag,
|
348
|
+
:preserve_script => preserve_script, :parse => parse, :value => value)
|
349
|
+
end
|
350
|
+
|
351
|
+
# Renders a line that creates an XHTML tag and has an implicit div because of
|
352
|
+
# `.` or `#`.
|
353
|
+
def div(line)
|
354
|
+
tag('%div' + line)
|
355
|
+
end
|
356
|
+
|
357
|
+
# Renders an XHTML comment.
|
358
|
+
def comment(line)
|
359
|
+
conditional, line = balance(line, ?[, ?]) if line[0] == ?[
|
360
|
+
line.strip!
|
361
|
+
conditional << ">" if conditional
|
362
|
+
|
363
|
+
if block_opened? && !line.empty?
|
364
|
+
raise SyntaxError.new('Illegal nesting: nesting within a tag that already has content is illegal.', @next_line.index)
|
365
|
+
end
|
366
|
+
|
367
|
+
ParseNode.new(:comment, @index, :conditional => conditional, :text => line)
|
368
|
+
end
|
369
|
+
|
370
|
+
# Renders an XHTML doctype or XML shebang.
|
371
|
+
def doctype(line)
|
372
|
+
raise SyntaxError.new("Illegal nesting: nesting within a header command is illegal.", @next_line.index) if block_opened?
|
373
|
+
version, type, encoding = line[3..-1].strip.downcase.scan(DOCTYPE_REGEX)[0]
|
374
|
+
ParseNode.new(:doctype, @index, :version => version, :type => type, :encoding => encoding)
|
375
|
+
end
|
376
|
+
|
377
|
+
def filter(name)
|
378
|
+
raise Error.new("Invalid filter name \":#{name}\".") unless name =~ /^\w+$/
|
379
|
+
|
380
|
+
@filter_buffer = String.new
|
381
|
+
|
382
|
+
if filter_opened?
|
383
|
+
@flat = true
|
384
|
+
# If we don't know the indentation by now, it'll be set in Line#tabs
|
385
|
+
@flat_spaces = @indentation * (@template_tabs+1) if @indentation
|
386
|
+
end
|
387
|
+
|
388
|
+
ParseNode.new(:filter, @index, :name => name, :text => @filter_buffer)
|
389
|
+
end
|
390
|
+
|
391
|
+
def close
|
392
|
+
node, @parent = @parent, @parent.parent
|
393
|
+
@template_tabs -= 1
|
394
|
+
send("close_#{node.type}", node) if respond_to?("close_#{node.type}", :include_private)
|
395
|
+
end
|
396
|
+
|
397
|
+
def close_filter(_)
|
398
|
+
@flat = false
|
399
|
+
@flat_spaces = nil
|
400
|
+
@filter_buffer = nil
|
401
|
+
end
|
402
|
+
|
403
|
+
def close_haml_comment(_)
|
404
|
+
@haml_comment = false
|
405
|
+
end
|
406
|
+
|
407
|
+
def close_silent_script(node)
|
408
|
+
# Post-process case statements to normalize the nesting of "when" clauses
|
409
|
+
return unless node.value[:keyword] == "case"
|
410
|
+
return unless first = node.children.first
|
411
|
+
return unless first.type == :silent_script && first.value[:keyword] == "when"
|
412
|
+
return if first.children.empty?
|
413
|
+
# If the case node has a "when" child with children, it's the
|
414
|
+
# only child. Then we want to put everything nested beneath it
|
415
|
+
# beneath the case itself (just like "if").
|
416
|
+
node.children = [first, *first.children]
|
417
|
+
first.children = []
|
418
|
+
end
|
419
|
+
|
420
|
+
# This is a class method so it can be accessed from {Haml::Helpers}.
|
421
|
+
#
|
422
|
+
# Iterates through the classes and ids supplied through `.`
|
423
|
+
# and `#` syntax, and returns a hash with them as attributes,
|
424
|
+
# that can then be merged with another attributes hash.
|
425
|
+
def self.parse_class_and_id(list)
|
426
|
+
attributes = {}
|
427
|
+
list.scan(/([#.])([-:_a-zA-Z0-9]+)/) do |type, property|
|
428
|
+
case type
|
429
|
+
when '.'
|
430
|
+
if attributes['class']
|
431
|
+
attributes['class'] += " "
|
432
|
+
else
|
433
|
+
attributes['class'] = ""
|
434
|
+
end
|
435
|
+
attributes['class'] += property
|
436
|
+
when '#'; attributes['id'] = property
|
437
|
+
end
|
438
|
+
end
|
439
|
+
attributes
|
440
|
+
end
|
441
|
+
|
442
|
+
def parse_static_hash(text)
|
443
|
+
attributes = {}
|
444
|
+
scanner = StringScanner.new(text)
|
445
|
+
scanner.scan(/\s+/)
|
446
|
+
until scanner.eos?
|
447
|
+
return unless key = scanner.scan(LITERAL_VALUE_REGEX)
|
448
|
+
return unless scanner.scan(/\s*=>\s*/)
|
449
|
+
return unless value = scanner.scan(LITERAL_VALUE_REGEX)
|
450
|
+
return unless scanner.scan(/\s*(?:,|$)\s*/)
|
451
|
+
attributes[eval(key).to_s] = eval(value).to_s
|
452
|
+
end
|
453
|
+
attributes
|
454
|
+
end
|
455
|
+
|
456
|
+
# Parses a line into tag_name, attributes, attributes_hash, object_ref, action, value
|
457
|
+
def parse_tag(line)
|
458
|
+
raise SyntaxError.new("Invalid tag: \"#{line}\".") unless match = line.scan(/%([-:\w]+)([-:\w\.\#]*)(.*)/)[0]
|
459
|
+
|
460
|
+
tag_name, attributes, rest = match
|
461
|
+
raise SyntaxError.new("Illegal element: classes and ids must have values.") if attributes =~ /[\.#](\.|#|\z)/
|
462
|
+
|
463
|
+
new_attributes_hash = old_attributes_hash = last_line = nil
|
464
|
+
object_ref = "nil"
|
465
|
+
attributes_hashes = []
|
466
|
+
while rest
|
467
|
+
case rest[0]
|
468
|
+
when ?{
|
469
|
+
break if old_attributes_hash
|
470
|
+
old_attributes_hash, rest, last_line = parse_old_attributes(rest)
|
471
|
+
attributes_hashes << [:old, old_attributes_hash]
|
472
|
+
when ?(
|
473
|
+
break if new_attributes_hash
|
474
|
+
new_attributes_hash, rest, last_line = parse_new_attributes(rest)
|
475
|
+
attributes_hashes << [:new, new_attributes_hash]
|
476
|
+
when ?[
|
477
|
+
break unless object_ref == "nil"
|
478
|
+
object_ref, rest = balance(rest, ?[, ?])
|
479
|
+
else; break
|
480
|
+
end
|
481
|
+
end
|
482
|
+
|
483
|
+
if rest
|
484
|
+
nuke_whitespace, action, value = rest.scan(/(<>|><|[><])?([=\/\~&!])?(.*)?/)[0]
|
485
|
+
nuke_whitespace ||= ''
|
486
|
+
nuke_outer_whitespace = nuke_whitespace.include? '>'
|
487
|
+
nuke_inner_whitespace = nuke_whitespace.include? '<'
|
488
|
+
end
|
489
|
+
|
490
|
+
value = value.to_s.strip
|
491
|
+
[tag_name, attributes, attributes_hashes, object_ref, nuke_outer_whitespace,
|
492
|
+
nuke_inner_whitespace, action, value, last_line || @index]
|
493
|
+
end
|
494
|
+
|
495
|
+
def parse_old_attributes(line)
|
496
|
+
line = line.dup
|
497
|
+
last_line = @index
|
498
|
+
|
499
|
+
begin
|
500
|
+
attributes_hash, rest = balance(line, ?{, ?})
|
501
|
+
rescue SyntaxError => e
|
502
|
+
if line.strip[-1] == ?, && e.message == "Unbalanced brackets."
|
503
|
+
line << "\n" << @next_line.text
|
504
|
+
last_line += 1
|
505
|
+
next_line
|
506
|
+
retry
|
507
|
+
end
|
508
|
+
|
509
|
+
raise e
|
510
|
+
end
|
511
|
+
|
512
|
+
attributes_hash = attributes_hash[1...-1] if attributes_hash
|
513
|
+
return attributes_hash, rest, last_line
|
514
|
+
end
|
515
|
+
|
516
|
+
def parse_new_attributes(line)
|
517
|
+
line = line.dup
|
518
|
+
scanner = StringScanner.new(line)
|
519
|
+
last_line = @index
|
520
|
+
attributes = {}
|
521
|
+
|
522
|
+
scanner.scan(/\(\s*/)
|
523
|
+
loop do
|
524
|
+
name, value = parse_new_attribute(scanner)
|
525
|
+
break if name.nil?
|
526
|
+
|
527
|
+
if name == false
|
528
|
+
text = (Haml::Shared.balance(line, ?(, ?)) || [line]).first
|
529
|
+
raise Haml::SyntaxError.new("Invalid attribute list: #{text.inspect}.", last_line - 1)
|
530
|
+
end
|
531
|
+
attributes[name] = value
|
532
|
+
scanner.scan(/\s*/)
|
533
|
+
|
534
|
+
if scanner.eos?
|
535
|
+
line << " " << @next_line.text
|
536
|
+
last_line += 1
|
537
|
+
next_line
|
538
|
+
scanner.scan(/\s*/)
|
539
|
+
end
|
540
|
+
end
|
541
|
+
|
542
|
+
static_attributes = {}
|
543
|
+
dynamic_attributes = "{"
|
544
|
+
attributes.each do |name, (type, val)|
|
545
|
+
if type == :static
|
546
|
+
static_attributes[name] = val
|
547
|
+
else
|
548
|
+
dynamic_attributes << inspect_obj(name) << " => " << val << ","
|
549
|
+
end
|
550
|
+
end
|
551
|
+
dynamic_attributes << "}"
|
552
|
+
dynamic_attributes = nil if dynamic_attributes == "{}"
|
553
|
+
|
554
|
+
return [static_attributes, dynamic_attributes], scanner.rest, last_line
|
555
|
+
end
|
556
|
+
|
557
|
+
def parse_new_attribute(scanner)
|
558
|
+
unless name = scanner.scan(/[-:\w]+/)
|
559
|
+
return if scanner.scan(/\)/)
|
560
|
+
return false
|
561
|
+
end
|
562
|
+
|
563
|
+
scanner.scan(/\s*/)
|
564
|
+
return name, [:static, true] unless scanner.scan(/=/) #/end
|
565
|
+
|
566
|
+
scanner.scan(/\s*/)
|
567
|
+
unless quote = scanner.scan(/["']/)
|
568
|
+
return false unless var = scanner.scan(/(@@?|\$)?\w+/)
|
569
|
+
return name, [:dynamic, var]
|
570
|
+
end
|
571
|
+
|
572
|
+
re = /((?:\\.|\#(?!\{)|[^#{quote}\\#])*)(#{quote}|#\{)/
|
573
|
+
content = []
|
574
|
+
loop do
|
575
|
+
return false unless scanner.scan(re)
|
576
|
+
content << [:str, scanner[1].gsub(/\\(.)/, '\1')]
|
577
|
+
break if scanner[2] == quote
|
578
|
+
content << [:ruby, balance(scanner, ?{, ?}, 1).first[0...-1]]
|
579
|
+
end
|
580
|
+
|
581
|
+
return name, [:static, content.first[1]] if content.size == 1
|
582
|
+
return name, [:dynamic,
|
583
|
+
'"' + content.map {|(t, v)| t == :str ? inspect_obj(v)[1...-1] : "\#{#{v}}"}.join + '"']
|
584
|
+
end
|
585
|
+
|
586
|
+
def raw_next_line
|
587
|
+
text = @template.shift
|
588
|
+
return unless text
|
589
|
+
|
590
|
+
index = @template_index
|
591
|
+
@template_index += 1
|
592
|
+
|
593
|
+
return text, index
|
594
|
+
end
|
595
|
+
|
596
|
+
def next_line
|
597
|
+
text, index = raw_next_line
|
598
|
+
return unless text
|
599
|
+
|
600
|
+
# :eod is a special end-of-document marker
|
601
|
+
line =
|
602
|
+
if text == :eod
|
603
|
+
Line.new '-#', '-#', '-#', index, self, true
|
604
|
+
else
|
605
|
+
Line.new text.strip, text.lstrip.chomp, text, index, self, false
|
606
|
+
end
|
607
|
+
|
608
|
+
# `flat?' here is a little outdated,
|
609
|
+
# so we have to manually check if either the previous or current line
|
610
|
+
# closes the flat block, as well as whether a new block is opened.
|
611
|
+
@line.tabs if @line
|
612
|
+
unless (flat? && !closes_flat?(line) && !closes_flat?(@line)) ||
|
613
|
+
(@line && @line.text[0] == ?: && line.full =~ %r[^#{@line.full[/^\s+/]}\s])
|
614
|
+
return next_line if line.text.empty?
|
615
|
+
|
616
|
+
handle_multiline(line)
|
617
|
+
end
|
618
|
+
|
619
|
+
@next_line = line
|
620
|
+
end
|
621
|
+
|
622
|
+
def closes_flat?(line)
|
623
|
+
line && !line.text.empty? && line.full !~ /^#{@flat_spaces}/
|
624
|
+
end
|
625
|
+
|
626
|
+
def un_next_line(line)
|
627
|
+
@template.unshift line
|
628
|
+
@template_index -= 1
|
629
|
+
end
|
630
|
+
|
631
|
+
def handle_multiline(line)
|
632
|
+
return unless is_multiline?(line.text)
|
633
|
+
line.text.slice!(-1)
|
634
|
+
while new_line = raw_next_line.first
|
635
|
+
break if new_line == :eod
|
636
|
+
next if new_line.strip.empty?
|
637
|
+
break unless is_multiline?(new_line.strip)
|
638
|
+
line.text << new_line.strip[0...-1]
|
639
|
+
end
|
640
|
+
un_next_line new_line
|
641
|
+
end
|
642
|
+
|
643
|
+
# Checks whether or not `line` is in a multiline sequence.
|
644
|
+
def is_multiline?(text)
|
645
|
+
text && text.length > 1 && text[-1] == MULTILINE_CHAR_VALUE && text[-2] == ?\s
|
646
|
+
end
|
647
|
+
|
648
|
+
def handle_ruby_multiline(text)
|
649
|
+
text = text.rstrip
|
650
|
+
return text unless is_ruby_multiline?(text)
|
651
|
+
un_next_line @next_line.full
|
652
|
+
begin
|
653
|
+
new_line = raw_next_line.first
|
654
|
+
break if new_line == :eod
|
655
|
+
next if new_line.strip.empty?
|
656
|
+
text << " " << new_line.strip
|
657
|
+
end while is_ruby_multiline?(new_line.strip)
|
658
|
+
next_line
|
659
|
+
text
|
660
|
+
end
|
661
|
+
|
662
|
+
def is_ruby_multiline?(text)
|
663
|
+
text && text.length > 1 && text[-1] == ?, && text[-2] != ?? && text[-3..-2] != "?\\"
|
664
|
+
end
|
665
|
+
|
666
|
+
def contains_interpolation?(str)
|
667
|
+
str.include?('#{')
|
668
|
+
end
|
669
|
+
|
670
|
+
def unescape_interpolation(str, escape_html = nil)
|
671
|
+
res = ''
|
672
|
+
rest = Haml::Shared.handle_interpolation str.dump do |scan|
|
673
|
+
escapes = (scan[2].size - 1) / 2
|
674
|
+
res << scan.matched[0...-3 - escapes]
|
675
|
+
if escapes % 2 == 1
|
676
|
+
res << '#{'
|
677
|
+
else
|
678
|
+
content = eval('"' + balance(scan, ?{, ?}, 1)[0][0...-1] + '"')
|
679
|
+
content = "Haml::Helpers.html_escape((#{content}))" if escape_html
|
680
|
+
res << '#{' + content + "}"# Use eval to get rid of string escapes
|
681
|
+
end
|
682
|
+
end
|
683
|
+
res + rest
|
684
|
+
end
|
685
|
+
|
686
|
+
def balance(*args)
|
687
|
+
res = Haml::Shared.balance(*args)
|
688
|
+
return res if res
|
689
|
+
raise SyntaxError.new("Unbalanced brackets.")
|
690
|
+
end
|
691
|
+
|
692
|
+
def block_opened?
|
693
|
+
@next_line.tabs > @line.tabs
|
694
|
+
end
|
695
|
+
|
696
|
+
# Same semantics as block_opened?, except that block_opened? uses Line#tabs,
|
697
|
+
# which doesn't interact well with filter lines
|
698
|
+
def filter_opened?
|
699
|
+
@next_line.full =~ (@indentation ? /^#{@indentation * @template_tabs}/ : /^\s/)
|
700
|
+
end
|
701
|
+
|
702
|
+
def flat?
|
703
|
+
@flat
|
704
|
+
end
|
705
|
+
end
|
706
|
+
end
|