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.

@@ -1,6 +1,7 @@
1
1
  require 'haml/helpers'
2
2
  require 'haml/buffer'
3
- require 'haml/precompiler'
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 Precompiler
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
- precompile
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}"
@@ -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::Precompiler} instance to compile the string to Ruby code
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::Precompiler} interface is neither well-documented
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 precompiler [Haml::Precompiler] The precompiler instance
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(precompiler, text)
96
+ def compile(compiler, text)
97
97
  resolve_lazy_requires
98
98
  filter = self
99
- precompiler.instance_eval do
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 if text.gsub!(/\n"\Z/, "\\n\"")
109
- push_script <<RUBY.strip, :escape_html => false
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, precompiler.options), precompiler.options[:preserve])
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(precompiler, text)
274
- return if precompiler.options[:suppress_eval]
275
- precompiler.instance_eval do
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(precompiler, text)
322
- return if precompiler.options[:suppress_eval]
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
- precompiler.send(:push_silent, src)
327
+ compiler.send(:push_silent, src)
326
328
  end
327
329
  end
328
330
 
@@ -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::Precompiler.build_attributes(haml_buffer.html?,
449
- haml_buffer.options[:attr_wrapper],
450
- haml_buffer.options[:escape_attrs],
451
- attrs)
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
- Precompiler.parse_class_and_id($2), attributes_hash)
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.
@@ -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