haml 6.0.0.beta.1-java

Sign up to get free protection for your applications and to get access to all the features.
Files changed (74) hide show
  1. checksums.yaml +7 -0
  2. data/.github/FUNDING.yml +1 -0
  3. data/.github/workflows/test.yml +40 -0
  4. data/.gitignore +19 -0
  5. data/CHANGELOG.md +1515 -0
  6. data/FAQ.md +147 -0
  7. data/Gemfile +23 -0
  8. data/MIT-LICENSE +20 -0
  9. data/README.md +210 -0
  10. data/REFERENCE.md +1380 -0
  11. data/Rakefile +116 -0
  12. data/bin/bench +66 -0
  13. data/bin/console +11 -0
  14. data/bin/ruby +3 -0
  15. data/bin/setup +7 -0
  16. data/bin/stackprof +27 -0
  17. data/bin/test +24 -0
  18. data/exe/haml +6 -0
  19. data/ext/haml/extconf.rb +10 -0
  20. data/ext/haml/haml.c +537 -0
  21. data/ext/haml/hescape.c +108 -0
  22. data/ext/haml/hescape.h +20 -0
  23. data/haml.gemspec +47 -0
  24. data/lib/haml/ambles.rb +20 -0
  25. data/lib/haml/attribute_builder.rb +175 -0
  26. data/lib/haml/attribute_compiler.rb +128 -0
  27. data/lib/haml/attribute_parser.rb +110 -0
  28. data/lib/haml/cli.rb +154 -0
  29. data/lib/haml/compiler/children_compiler.rb +126 -0
  30. data/lib/haml/compiler/comment_compiler.rb +39 -0
  31. data/lib/haml/compiler/doctype_compiler.rb +46 -0
  32. data/lib/haml/compiler/script_compiler.rb +116 -0
  33. data/lib/haml/compiler/silent_script_compiler.rb +24 -0
  34. data/lib/haml/compiler/tag_compiler.rb +76 -0
  35. data/lib/haml/compiler.rb +97 -0
  36. data/lib/haml/dynamic_merger.rb +67 -0
  37. data/lib/haml/engine.rb +53 -0
  38. data/lib/haml/error.rb +16 -0
  39. data/lib/haml/escapable.rb +13 -0
  40. data/lib/haml/filters/base.rb +12 -0
  41. data/lib/haml/filters/cdata.rb +20 -0
  42. data/lib/haml/filters/coffee.rb +17 -0
  43. data/lib/haml/filters/css.rb +33 -0
  44. data/lib/haml/filters/erb.rb +10 -0
  45. data/lib/haml/filters/escaped.rb +22 -0
  46. data/lib/haml/filters/javascript.rb +33 -0
  47. data/lib/haml/filters/less.rb +20 -0
  48. data/lib/haml/filters/markdown.rb +11 -0
  49. data/lib/haml/filters/plain.rb +29 -0
  50. data/lib/haml/filters/preserve.rb +22 -0
  51. data/lib/haml/filters/ruby.rb +10 -0
  52. data/lib/haml/filters/sass.rb +15 -0
  53. data/lib/haml/filters/scss.rb +15 -0
  54. data/lib/haml/filters/text_base.rb +25 -0
  55. data/lib/haml/filters/tilt_base.rb +49 -0
  56. data/lib/haml/filters.rb +75 -0
  57. data/lib/haml/force_escapable.rb +29 -0
  58. data/lib/haml/haml_error.rb +66 -0
  59. data/lib/haml/helpers.rb +15 -0
  60. data/lib/haml/html.rb +22 -0
  61. data/lib/haml/identity.rb +13 -0
  62. data/lib/haml/object_ref.rb +30 -0
  63. data/lib/haml/parser.rb +986 -0
  64. data/lib/haml/rails_helpers.rb +51 -0
  65. data/lib/haml/rails_template.rb +55 -0
  66. data/lib/haml/railtie.rb +15 -0
  67. data/lib/haml/ruby_expression.rb +32 -0
  68. data/lib/haml/string_splitter.rb +20 -0
  69. data/lib/haml/template.rb +20 -0
  70. data/lib/haml/temple_line_counter.rb +31 -0
  71. data/lib/haml/util.rb +260 -0
  72. data/lib/haml/version.rb +4 -0
  73. data/lib/haml.rb +13 -0
  74. metadata +359 -0
@@ -0,0 +1,986 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ripper'
4
+ require 'strscan'
5
+ require 'haml/haml_error'
6
+ require 'haml/util'
7
+
8
+ module Haml
9
+ class Parser
10
+ include Haml::Util
11
+
12
+ attr_reader :root
13
+
14
+ # Designates an XHTML/XML element.
15
+ ELEMENT = ?%
16
+
17
+ # Designates a `<div>` element with the given class.
18
+ DIV_CLASS = ?.
19
+
20
+ # Designates a `<div>` element with the given id.
21
+ DIV_ID = ?#
22
+
23
+ # Designates an XHTML/XML comment.
24
+ COMMENT = ?/
25
+
26
+ # Designates an XHTML doctype or script that is never HTML-escaped.
27
+ DOCTYPE = ?!
28
+
29
+ # Designates script, the result of which is output.
30
+ SCRIPT = ?=
31
+
32
+ # Designates script that is always HTML-escaped.
33
+ SANITIZE = ?&
34
+
35
+ # Designates script, the result of which is flattened and output.
36
+ FLAT_SCRIPT = ?~
37
+
38
+ # Designates script which is run but not output.
39
+ SILENT_SCRIPT = ?-
40
+
41
+ # When following SILENT_SCRIPT, designates a comment that is not output.
42
+ SILENT_COMMENT = ?#
43
+
44
+ # Designates a non-parsed line.
45
+ ESCAPE = ?\\
46
+
47
+ # Designates a block of filtered text.
48
+ FILTER = ?:
49
+
50
+ # Designates a non-parsed line. Not actually a character.
51
+ PLAIN_TEXT = -1
52
+
53
+ # Keeps track of the ASCII values of the characters that begin a
54
+ # specially-interpreted line.
55
+ SPECIAL_CHARACTERS = [
56
+ ELEMENT,
57
+ DIV_CLASS,
58
+ DIV_ID,
59
+ COMMENT,
60
+ DOCTYPE,
61
+ SCRIPT,
62
+ SANITIZE,
63
+ FLAT_SCRIPT,
64
+ SILENT_SCRIPT,
65
+ ESCAPE,
66
+ FILTER
67
+ ].freeze
68
+
69
+ # The value of the character that designates that a line is part
70
+ # of a multiline string.
71
+ MULTILINE_CHAR_VALUE = ?|
72
+
73
+ # Regex to check for blocks with spaces around arguments. Not to be confused
74
+ # with multiline script.
75
+ # For example:
76
+ # foo.each do | bar |
77
+ # = bar
78
+ #
79
+ BLOCK_WITH_SPACES = /do\s*\|\s*[^\|]*\s+\|\z/
80
+
81
+ MID_BLOCK_KEYWORDS = %w[else elsif rescue ensure end when].freeze
82
+ START_BLOCK_KEYWORDS = %w[if begin case unless].freeze
83
+ # Try to parse assignments to block starters as best as possible
84
+ START_BLOCK_KEYWORD_REGEX = /(?:\w+(?:,\s*\w+)*\s*=\s*)?(#{START_BLOCK_KEYWORDS.join('|')})/
85
+ BLOCK_KEYWORD_REGEX = /^-?\s*(?:(#{MID_BLOCK_KEYWORDS.join('|')})|#{START_BLOCK_KEYWORD_REGEX.source})\b/
86
+
87
+ # The Regex that matches a Doctype command.
88
+ DOCTYPE_REGEX = /(\d(?:\.\d)?)?\s*([a-z]*)\s*([^ ]+)?/i
89
+
90
+ # The Regex that matches a literal string or symbol value
91
+ LITERAL_VALUE_REGEX = /:(\w*)|(["'])((?!\\|\#\{|\#@|\#\$|\2).|\\.)*\2/
92
+
93
+ ID_KEY = 'id'.freeze
94
+ CLASS_KEY = 'class'.freeze
95
+
96
+ # Used for scanning old attributes, substituting the first '{'
97
+ METHOD_CALL_PREFIX = 'a('
98
+
99
+ def initialize(options)
100
+ @options = ParserOptions.new(options)
101
+ # Record the indent levels of "if" statements to validate the subsequent
102
+ # elsif and else statements are indented at the appropriate level.
103
+ @script_level_stack = []
104
+ @template_index = 0
105
+ @template_tabs = 0
106
+ end
107
+
108
+ def call(template)
109
+ template = Haml::Util.check_haml_encoding(template) do |msg, line|
110
+ raise Haml::Error.new(msg, line)
111
+ end
112
+
113
+ match = template.rstrip.scan(/(([ \t]+)?(.*?))(?:\Z|\r\n|\r|\n)/m)
114
+ # discard the last match which is always blank
115
+ match.pop
116
+ @template = match.each_with_index.map do |(full, whitespace, text), index|
117
+ Line.new(whitespace, text.rstrip, full, index, self, false)
118
+ end
119
+ # Append special end-of-document marker
120
+ @template << Line.new(nil, '-#', '-#', @template.size, self, true)
121
+
122
+ @root = @parent = ParseNode.new(:root)
123
+ @flat = false
124
+ @filter_buffer = nil
125
+ @indentation = nil
126
+ @line = next_line
127
+
128
+ raise HamlSyntaxError.new(HamlError.message(:indenting_at_start), @line.index) if @line.tabs != 0
129
+
130
+ loop do
131
+ next_line
132
+
133
+ process_indent(@line) unless @line.text.empty?
134
+
135
+ if flat?
136
+ text = @line.full.dup
137
+ text = "" unless text.gsub!(/^#{@flat_spaces}/, '')
138
+ @filter_buffer << "#{text}\n"
139
+ @line = @next_line
140
+ next
141
+ end
142
+
143
+ @tab_up = nil
144
+ process_line(@line) unless @line.text.empty?
145
+ if block_opened? || @tab_up
146
+ @template_tabs += 1
147
+ @parent = @parent.children.last
148
+ end
149
+
150
+ if !flat? && @next_line.tabs - @line.tabs > 1
151
+ raise HamlSyntaxError.new(HamlError.message(:deeper_indenting, @next_line.tabs - @line.tabs), @next_line.index)
152
+ end
153
+
154
+ @line = @next_line
155
+ end
156
+ # Close all the open tags
157
+ close until @parent.type == :root
158
+ @root
159
+ rescue Haml::HamlError => e
160
+ e.backtrace.unshift "#{@options.filename}:#{(e.line ? e.line + 1 : @line.index + 1) + @options.line - 1}"
161
+ error_with_lineno(e)
162
+ end
163
+
164
+ def compute_tabs(line)
165
+ return 0 if line.text.empty? || !line.whitespace
166
+
167
+ if @indentation.nil?
168
+ @indentation = line.whitespace
169
+
170
+ if @indentation.include?(?\s) && @indentation.include?(?\t)
171
+ raise HamlSyntaxError.new(HamlError.message(:cant_use_tabs_and_spaces), line.index)
172
+ end
173
+
174
+ @flat_spaces = @indentation * (@template_tabs+1) if flat?
175
+ return 1
176
+ end
177
+
178
+ tabs = line.whitespace.length / @indentation.length
179
+ return tabs if line.whitespace == @indentation * tabs
180
+ return @template_tabs + 1 if flat? && line.whitespace =~ /^#{@flat_spaces}/
181
+
182
+ message = HamlError.message(:inconsistent_indentation,
183
+ human_indentation(line.whitespace),
184
+ human_indentation(@indentation)
185
+ )
186
+ raise HamlSyntaxError.new(message, line.index)
187
+ end
188
+
189
+ private
190
+
191
+ def error_with_lineno(error)
192
+ return error if error.line
193
+
194
+ trace = error.backtrace.first
195
+ return error unless trace
196
+
197
+ line = trace.match(/\d+\z/).to_s.to_i
198
+ HamlSyntaxError.new(error.message, line)
199
+ end
200
+
201
+ # @private
202
+ Line = Struct.new(:whitespace, :text, :full, :index, :parser, :eod) do
203
+ alias_method :eod?, :eod
204
+
205
+ # @private
206
+ def tabs
207
+ @tabs ||= parser.compute_tabs(self)
208
+ end
209
+
210
+ def strip!(from)
211
+ self.text = text[from..-1]
212
+ self.text.lstrip!
213
+ self
214
+ end
215
+ end
216
+
217
+ # @private
218
+ ParseNode = Struct.new(:type, :line, :value, :parent, :children) do
219
+ def initialize(*args)
220
+ super
221
+ self.children ||= []
222
+ end
223
+
224
+ def inspect
225
+ %Q[(#{type} #{value.inspect}#{children.each_with_object(''.dup) {|c, s| s << "\n#{c.inspect.gsub!(/^/, ' ')}"}})].dup
226
+ end
227
+ end
228
+
229
+ # @param [String] new - Hash literal including dynamic values.
230
+ # @param [String] old - Hash literal including dynamic values or Ruby literal of multiple Hashes which MUST be interpreted as method's last arguments.
231
+ DynamicAttributes = Struct.new(:new, :old) do
232
+ undef :old=
233
+ def old=(value)
234
+ unless value =~ /\A{.*}\z/m
235
+ raise ArgumentError.new('Old attributes must start with "{" and end with "}"')
236
+ end
237
+ self[:old] = value
238
+ end
239
+
240
+ # This will be a literal for Haml::HamlBuffer#attributes's last argument, `attributes_hashes`.
241
+ def to_literal
242
+ [new, stripped_old].compact.join(', ')
243
+ end
244
+
245
+ private
246
+
247
+ # For `%foo{ { foo: 1 }, bar: 2 }`, :old is "{ { foo: 1 }, bar: 2 }" and this method returns " { foo: 1 }, bar: 2 " for last argument.
248
+ def stripped_old
249
+ return nil if old.nil?
250
+ old.sub!(/\A{/, '').sub!(/}\z/m, '')
251
+ end
252
+ end
253
+
254
+ # Processes and deals with lowering indentation.
255
+ def process_indent(line)
256
+ return unless line.tabs <= @template_tabs && @template_tabs > 0
257
+
258
+ to_close = @template_tabs - line.tabs
259
+ to_close.times {|i| close unless to_close - 1 - i == 0 && continuation_script?(line.text)}
260
+ end
261
+
262
+ def continuation_script?(text)
263
+ text[0] == SILENT_SCRIPT && mid_block_keyword?(text)
264
+ end
265
+
266
+ def mid_block_keyword?(text)
267
+ MID_BLOCK_KEYWORDS.include?(block_keyword(text))
268
+ end
269
+
270
+ # Processes a single line of Haml.
271
+ #
272
+ # This method doesn't return anything; it simply processes the line and
273
+ # adds the appropriate code to `@precompiled`.
274
+ def process_line(line)
275
+ case line.text[0]
276
+ when DIV_CLASS; push div(line)
277
+ when DIV_ID
278
+ return push plain(line) if %w[{ @ $].include?(line.text[1])
279
+ push div(line)
280
+ when ELEMENT; push tag(line)
281
+ when COMMENT; push comment(line.text[1..-1].lstrip)
282
+ when SANITIZE
283
+ return push plain(line.strip!(3), :escape_html) if line.text[1, 2] == '=='
284
+ return push script(line.strip!(2), :escape_html) if line.text[1] == SCRIPT
285
+ return push flat_script(line.strip!(2), :escape_html) if line.text[1] == FLAT_SCRIPT
286
+ return push plain(line.strip!(1), :escape_html) if line.text[1] == ?\s || line.text[1..2] == '#{'
287
+ push plain(line)
288
+ when SCRIPT
289
+ return push plain(line.strip!(2)) if line.text[1] == SCRIPT
290
+ line.text = line.text[1..-1]
291
+ push script(line)
292
+ when FLAT_SCRIPT; push flat_script(line.strip!(1))
293
+ when SILENT_SCRIPT
294
+ return push haml_comment(line.text[2..-1]) if line.text[1] == SILENT_COMMENT
295
+ push silent_script(line)
296
+ when FILTER; push filter(line.text[1..-1].downcase)
297
+ when DOCTYPE
298
+ return push doctype(line.text) if line.text[0, 3] == '!!!'
299
+ return push plain(line.strip!(3), false) if line.text[1, 2] == '=='
300
+ return push script(line.strip!(2), false) if line.text[1] == SCRIPT
301
+ return push flat_script(line.strip!(2), false) if line.text[1] == FLAT_SCRIPT
302
+ return push plain(line.strip!(1), false) if line.text[1] == ?\s || line.text[1..2] == '#{'
303
+ push plain(line)
304
+ when ESCAPE
305
+ line.text = line.text[1..-1]
306
+ push plain(line)
307
+ else; push plain(line)
308
+ end
309
+ end
310
+
311
+ def block_keyword(text)
312
+ return unless (keyword = text.scan(BLOCK_KEYWORD_REGEX)[0])
313
+ keyword[0] || keyword[1]
314
+ end
315
+
316
+ def push(node)
317
+ @parent.children << node
318
+ node.parent = @parent
319
+ end
320
+
321
+ def plain(line, escape_html = nil)
322
+ if block_opened?
323
+ raise HamlSyntaxError.new(HamlError.message(:illegal_nesting_plain), @next_line.index)
324
+ end
325
+
326
+ unless Util.contains_interpolation?(line.text)
327
+ return ParseNode.new(:plain, line.index + 1, :text => line.text)
328
+ end
329
+
330
+ escape_html = @options.escape_html && @options.mime_type != 'text/plain' if escape_html.nil?
331
+ line.text = Util.unescape_interpolation(line.text)
332
+ script(line, false).tap { |n| n.value[:escape_interpolation] = true if escape_html }
333
+ end
334
+
335
+ def script(line, escape_html = nil, preserve = false)
336
+ raise HamlSyntaxError.new(HamlError.message(:no_ruby_code, '=')) if line.text.empty?
337
+ line = handle_ruby_multiline(line)
338
+ escape_html = @options.escape_html if escape_html.nil?
339
+
340
+ keyword = block_keyword(line.text)
341
+ check_push_script_stack(keyword)
342
+
343
+ ParseNode.new(:script, line.index + 1, :text => line.text, :escape_html => escape_html,
344
+ :preserve => preserve, :keyword => keyword)
345
+ end
346
+
347
+ def flat_script(line, escape_html = nil)
348
+ raise HamlSyntaxError.new(HamlError.message(:no_ruby_code, '~')) if line.text.empty?
349
+ script(line, escape_html, :preserve)
350
+ end
351
+
352
+ def silent_script(line)
353
+ raise HamlSyntaxError.new(HamlError.message(:no_end), line.index) if line.text[1..-1].strip == 'end'
354
+
355
+ line = handle_ruby_multiline(line)
356
+ keyword = block_keyword(line.text)
357
+
358
+ check_push_script_stack(keyword)
359
+
360
+ if ["else", "elsif", "when"].include?(keyword)
361
+ if @script_level_stack.empty?
362
+ raise Haml::HamlSyntaxError.new(HamlError.message(:missing_if, keyword), @line.index)
363
+ end
364
+
365
+ if keyword == 'when' and !@script_level_stack.last[2]
366
+ if @script_level_stack.last[1] + 1 == @line.tabs
367
+ @script_level_stack.last[1] += 1
368
+ end
369
+ @script_level_stack.last[2] = true
370
+ end
371
+
372
+ if @script_level_stack.last[1] != @line.tabs
373
+ message = HamlError.message(:bad_script_indent, keyword, @script_level_stack.last[1], @line.tabs)
374
+ raise Haml::HamlSyntaxError.new(message, @line.index)
375
+ end
376
+ end
377
+
378
+ ParseNode.new(:silent_script, @line.index + 1,
379
+ :text => line.text[1..-1], :keyword => keyword)
380
+ end
381
+
382
+ def check_push_script_stack(keyword)
383
+ if ["if", "case", "unless"].include?(keyword)
384
+ # @script_level_stack contents are arrays of form
385
+ # [:keyword, stack_level, other_info]
386
+ @script_level_stack.push([keyword.to_sym, @line.tabs])
387
+ @script_level_stack.last << false if keyword == 'case'
388
+ @tab_up = true
389
+ end
390
+ end
391
+
392
+ def haml_comment(text)
393
+ if filter_opened?
394
+ @flat = true
395
+ @filter_buffer = String.new
396
+ @filter_buffer << "#{text}\n" unless text.empty?
397
+ text = @filter_buffer
398
+ # If we don't know the indentation by now, it'll be set in Line#tabs
399
+ @flat_spaces = @indentation * (@template_tabs+1) if @indentation
400
+ end
401
+
402
+ ParseNode.new(:haml_comment, @line.index + 1, :text => text)
403
+ end
404
+
405
+ def tag(line)
406
+ tag_name, attributes, attributes_hashes, object_ref, nuke_outer_whitespace,
407
+ nuke_inner_whitespace, action, value, last_line = parse_tag(line.text)
408
+
409
+ preserve_tag = @options.preserve.include?(tag_name)
410
+ nuke_inner_whitespace ||= preserve_tag
411
+ escape_html = (action == '&' || (action != '!' && @options.escape_html))
412
+
413
+ case action
414
+ when '/'; self_closing = true
415
+ when '~'; parse = preserve_script = true
416
+ when '='
417
+ parse = true
418
+ if value[0] == ?=
419
+ value = Util.unescape_interpolation(value[1..-1].strip)
420
+ escape_interpolation = true if escape_html
421
+ escape_html = false
422
+ end
423
+ when '&', '!'
424
+ if value[0] == ?= || value[0] == ?~
425
+ parse = true
426
+ preserve_script = (value[0] == ?~)
427
+ if value[1] == ?=
428
+ value = Util.unescape_interpolation(value[2..-1].strip)
429
+ escape_interpolation = true if escape_html
430
+ escape_html = false
431
+ else
432
+ value = value[1..-1].strip
433
+ end
434
+ elsif Util.contains_interpolation?(value)
435
+ value = Util.unescape_interpolation(value)
436
+ escape_interpolation = true if escape_html
437
+ parse = true
438
+ escape_html = false
439
+ end
440
+ else
441
+ if Util.contains_interpolation?(value)
442
+ value = Util.unescape_interpolation(value, escape_html)
443
+ parse = true
444
+ escape_html = false
445
+ end
446
+ end
447
+
448
+ attributes = Parser.parse_class_and_id(attributes)
449
+ dynamic_attributes = DynamicAttributes.new
450
+
451
+ if attributes_hashes[:new]
452
+ static_attributes, attributes_hash = attributes_hashes[:new]
453
+ AttributeMerger.merge_attributes!(attributes, static_attributes) if static_attributes
454
+ dynamic_attributes.new = attributes_hash
455
+ end
456
+
457
+ if attributes_hashes[:old]
458
+ static_attributes = parse_static_hash(attributes_hashes[:old])
459
+ AttributeMerger.merge_attributes!(attributes, static_attributes) if static_attributes
460
+ dynamic_attributes.old = attributes_hashes[:old] unless static_attributes || @options.suppress_eval
461
+ end
462
+
463
+ raise HamlSyntaxError.new(HamlError.message(:illegal_nesting_self_closing), @next_line.index) if block_opened? && self_closing
464
+ raise HamlSyntaxError.new(HamlError.message(:no_ruby_code, action), last_line - 1) if parse && value.empty?
465
+ raise HamlSyntaxError.new(HamlError.message(:self_closing_content), last_line - 1) if self_closing && !value.empty?
466
+
467
+ if block_opened? && !value.empty? && !is_ruby_multiline?(value)
468
+ raise HamlSyntaxError.new(HamlError.message(:illegal_nesting_line, tag_name), @next_line.index)
469
+ end
470
+
471
+ self_closing ||= !!(!block_opened? && value.empty? && @options.autoclose.any? {|t| t === tag_name})
472
+ value = nil if value.empty? && (block_opened? || self_closing)
473
+ line.text = value
474
+ line = handle_ruby_multiline(line) if parse
475
+
476
+ ParseNode.new(:tag, line.index + 1, :name => tag_name, :attributes => attributes,
477
+ :dynamic_attributes => dynamic_attributes, :self_closing => self_closing,
478
+ :nuke_inner_whitespace => nuke_inner_whitespace,
479
+ :nuke_outer_whitespace => nuke_outer_whitespace, :object_ref => object_ref,
480
+ :escape_html => escape_html, :preserve_tag => preserve_tag,
481
+ :preserve_script => preserve_script, :parse => parse, :value => line.text,
482
+ :escape_interpolation => escape_interpolation)
483
+ end
484
+
485
+ # Renders a line that creates an XHTML tag and has an implicit div because of
486
+ # `.` or `#`.
487
+ def div(line)
488
+ line.text = "%div#{line.text}"
489
+ tag(line)
490
+ end
491
+
492
+ # Renders an XHTML comment.
493
+ def comment(text)
494
+ if text[0..1] == '!['
495
+ revealed = true
496
+ text = text[1..-1]
497
+ else
498
+ revealed = false
499
+ end
500
+
501
+ conditional, text = balance(text, ?[, ?]) if text[0] == ?[
502
+ text.strip!
503
+
504
+ if Util.contains_interpolation?(text)
505
+ parse = true
506
+ text = Util.unescape_interpolation(text)
507
+ else
508
+ parse = false
509
+ end
510
+
511
+ if block_opened? && !text.empty?
512
+ raise HamlSyntaxError.new(Haml::HamlError.message(:illegal_nesting_content), @next_line.index)
513
+ end
514
+
515
+ ParseNode.new(:comment, @line.index + 1, :conditional => conditional, :text => text, :revealed => revealed, :parse => parse)
516
+ end
517
+
518
+ # Renders an XHTML doctype or XML shebang.
519
+ def doctype(text)
520
+ raise HamlSyntaxError.new(HamlError.message(:illegal_nesting_header), @next_line.index) if block_opened?
521
+ version, type, encoding = text[3..-1].strip.downcase.scan(DOCTYPE_REGEX)[0]
522
+ ParseNode.new(:doctype, @line.index + 1, :version => version, :type => type, :encoding => encoding)
523
+ end
524
+
525
+ def filter(name)
526
+ raise HamlError.new(HamlError.message(:invalid_filter_name, name)) unless name =~ /^\w+$/
527
+
528
+ if filter_opened?
529
+ @flat = true
530
+ @filter_buffer = String.new
531
+ # If we don't know the indentation by now, it'll be set in Line#tabs
532
+ @flat_spaces = @indentation * (@template_tabs+1) if @indentation
533
+ end
534
+
535
+ ParseNode.new(:filter, @line.index + 1, :name => name, :text => @filter_buffer)
536
+ end
537
+
538
+ def close
539
+ node, @parent = @parent, @parent.parent
540
+ @template_tabs -= 1
541
+ send("close_#{node.type}", node) if respond_to?("close_#{node.type}", :include_private)
542
+ end
543
+
544
+ def close_filter(_)
545
+ close_flat_section
546
+ end
547
+
548
+ def close_haml_comment(_)
549
+ close_flat_section
550
+ end
551
+
552
+ def close_flat_section
553
+ @flat = false
554
+ @flat_spaces = nil
555
+ @filter_buffer = nil
556
+ end
557
+
558
+ def close_silent_script(node)
559
+ @script_level_stack.pop if ["if", "case", "unless"].include? node.value[:keyword]
560
+
561
+ # Post-process case statements to normalize the nesting of "when" clauses
562
+ return unless node.value[:keyword] == "case"
563
+ return unless (first = node.children.first)
564
+ return unless first.type == :silent_script && first.value[:keyword] == "when"
565
+ return if first.children.empty?
566
+ # If the case node has a "when" child with children, it's the
567
+ # only child. Then we want to put everything nested beneath it
568
+ # beneath the case itself (just like "if").
569
+ node.children = [first, *first.children]
570
+ first.children = []
571
+ end
572
+
573
+ alias :close_script :close_silent_script
574
+
575
+ # This is a class method so it can be accessed from {Haml::HamlHelpers}.
576
+ #
577
+ # Iterates through the classes and ids supplied through `.`
578
+ # and `#` syntax, and returns a hash with them as attributes,
579
+ # that can then be merged with another attributes hash.
580
+ def self.parse_class_and_id(list)
581
+ attributes = {}
582
+ return attributes if list.empty?
583
+
584
+ list.scan(/([#.])([-:_a-zA-Z0-9\@]+)/) do |type, property|
585
+ case type
586
+ when '.'
587
+ if attributes[CLASS_KEY]
588
+ attributes[CLASS_KEY] += " "
589
+ else
590
+ attributes[CLASS_KEY] = ""
591
+ end
592
+ attributes[CLASS_KEY] += property
593
+ when '#'; attributes[ID_KEY] = property
594
+ end
595
+ end
596
+ attributes
597
+ end
598
+
599
+ # This method doesn't use Haml::HamlAttributeParser because currently it depends on Ripper and Rubinius doesn't provide it.
600
+ # Ideally this logic should be placed in Haml::HamlAttributeParser instead of here and this method should use it.
601
+ #
602
+ # @param [String] text - Hash literal or text inside old attributes
603
+ # @return [Hash,nil] - Return nil if text is not static Hash literal
604
+ def parse_static_hash(text)
605
+ attributes = {}
606
+ return attributes if text.empty?
607
+
608
+ text = text[1...-1] # strip brackets
609
+ scanner = StringScanner.new(text)
610
+ scanner.scan(/\s+/)
611
+ until scanner.eos?
612
+ return unless (key = scanner.scan(LITERAL_VALUE_REGEX))
613
+ return unless scanner.scan(/\s*=>\s*/)
614
+ return unless (value = scanner.scan(LITERAL_VALUE_REGEX))
615
+ return unless scanner.scan(/\s*(?:,|$)\s*/)
616
+ attributes[eval(key).to_s] = eval(value).to_s
617
+ end
618
+ attributes
619
+ end
620
+
621
+ # Parses a line into tag_name, attributes, attributes_hash, object_ref, action, value
622
+ def parse_tag(text)
623
+ match = text.scan(/%([-:\w]+)([-:\w.#\@]*)(.+)?/)[0]
624
+ raise HamlSyntaxError.new(HamlError.message(:invalid_tag, text)) unless match
625
+
626
+ tag_name, attributes, rest = match
627
+
628
+ if !attributes.empty? && (attributes =~ /[.#](\.|#|\z)/)
629
+ raise HamlSyntaxError.new(HamlError.message(:illegal_element))
630
+ end
631
+
632
+ new_attributes_hash = old_attributes_hash = last_line = nil
633
+ object_ref = :nil
634
+ attributes_hashes = {}
635
+ while rest && !rest.empty?
636
+ case rest[0]
637
+ when ?{
638
+ break if old_attributes_hash
639
+ old_attributes_hash, rest, last_line = parse_old_attributes(rest)
640
+ attributes_hashes[:old] = old_attributes_hash
641
+ when ?(
642
+ break if new_attributes_hash
643
+ new_attributes_hash, rest, last_line = parse_new_attributes(rest)
644
+ attributes_hashes[:new] = new_attributes_hash
645
+ when ?[
646
+ break unless object_ref == :nil
647
+ object_ref, rest = balance(rest, ?[, ?])
648
+ else; break
649
+ end
650
+ end
651
+
652
+ if rest && !rest.empty?
653
+ nuke_whitespace, action, value = rest.scan(/(<>|><|[><])?([=\/\~&!])?(.*)?/)[0]
654
+ if nuke_whitespace
655
+ nuke_outer_whitespace = nuke_whitespace.include? '>'
656
+ nuke_inner_whitespace = nuke_whitespace.include? '<'
657
+ end
658
+ end
659
+
660
+ if @options.remove_whitespace
661
+ nuke_outer_whitespace = true
662
+ nuke_inner_whitespace = true
663
+ end
664
+
665
+ if value.nil?
666
+ value = ''
667
+ else
668
+ value.strip!
669
+ end
670
+ [tag_name, attributes, attributes_hashes, object_ref, nuke_outer_whitespace,
671
+ nuke_inner_whitespace, action, value, last_line || @line.index + 1]
672
+ end
673
+
674
+ # @return [String] attributes_hash - Hash literal starting with `{` and ending with `}`
675
+ # @return [String] rest
676
+ # @return [Integer] last_line
677
+ def parse_old_attributes(text)
678
+ last_line = @line.index + 1
679
+
680
+ begin
681
+ # Old attributes often look like a valid Hash literal, but it sometimes allow code like
682
+ # `{ hash, foo: bar }`, which is compiled to `_hamlout.attributes({}, nil, hash, foo: bar)`.
683
+ #
684
+ # To scan such code correctly, this scans `a( hash, foo: bar }` instead, stops when there is
685
+ # 1 more :on_embexpr_end (the last '}') than :on_embexpr_beg, and resurrects '{' afterwards.
686
+ balanced, rest = balance_tokens(text.sub(?{, METHOD_CALL_PREFIX), :on_embexpr_beg, :on_embexpr_end, count: 1)
687
+ attributes_hash = balanced.sub(METHOD_CALL_PREFIX, ?{)
688
+ rescue HamlSyntaxError => e
689
+ if e.message == HamlError.message(:unbalanced_brackets) && !@template.empty?
690
+ text << "\n#{@next_line.text}"
691
+ last_line += 1
692
+ next_line
693
+ retry
694
+ end
695
+
696
+ raise e
697
+ end
698
+
699
+ return attributes_hash, rest, last_line
700
+ end
701
+
702
+ # @return [Array<Hash,String,nil>] - [static_attributes (Hash), dynamic_attributes (nil or String starting with `{` and ending with `}`)]
703
+ # @return [String] rest
704
+ # @return [Integer] last_line
705
+ def parse_new_attributes(text)
706
+ scanner = StringScanner.new(text)
707
+ last_line = @line.index + 1
708
+ attributes = {}
709
+
710
+ scanner.scan(/\(\s*/)
711
+ loop do
712
+ name, value = parse_new_attribute(scanner)
713
+ break if name.nil?
714
+
715
+ if name == false
716
+ scanned = Haml::Util.balance(text, ?(, ?))
717
+ text = scanned ? scanned.first : text
718
+ raise Haml::HamlSyntaxError.new(HamlError.message(:invalid_attribute_list, text.inspect), last_line - 1)
719
+ end
720
+ attributes[name] = value
721
+ scanner.scan(/\s*/)
722
+
723
+ if scanner.eos?
724
+ text << " #{@next_line.text}"
725
+ last_line += 1
726
+ next_line
727
+ scanner.scan(/\s*/)
728
+ end
729
+ end
730
+
731
+ static_attributes = {}
732
+ dynamic_attributes = "{".dup
733
+ attributes.each do |name, (type, val)|
734
+ if type == :static
735
+ static_attributes[name] = val
736
+ else
737
+ dynamic_attributes << "#{Util.inspect_obj(name)} => #{val},"
738
+ end
739
+ end
740
+ dynamic_attributes << "}"
741
+ dynamic_attributes = nil if dynamic_attributes == "{}"
742
+
743
+ return [static_attributes, dynamic_attributes], scanner.rest, last_line
744
+ end
745
+
746
+ def parse_new_attribute(scanner)
747
+ unless (name = scanner.scan(/[-:\w]+/))
748
+ return if scanner.scan(/\)/)
749
+ return false
750
+ end
751
+
752
+ scanner.scan(/\s*/)
753
+ return name, [:static, true] unless scanner.scan(/=/) #/end
754
+
755
+ scanner.scan(/\s*/)
756
+ unless (quote = scanner.scan(/["']/))
757
+ return false unless (var = scanner.scan(/(@@?|\$)?\w+/))
758
+ return name, [:dynamic, var]
759
+ end
760
+
761
+ re = /((?:\\.|\#(?!\{)|[^#{quote}\\#])*)(#{quote}|#\{)/
762
+ content = []
763
+ loop do
764
+ return false unless scanner.scan(re)
765
+ content << [:str, scanner[1].gsub(/\\(.)/, '\1')]
766
+ break if scanner[2] == quote
767
+ content << [:ruby, balance(scanner, ?{, ?}, 1).first[0...-1]]
768
+ end
769
+
770
+ return name, [:static, content.first[1]] if content.size == 1
771
+ return name, [:dynamic,
772
+ %!"#{content.each_with_object(''.dup) {|(t, v), s| s << (t == :str ? Util.inspect_obj(v)[1...-1] : "\#{#{v}}")}}"!]
773
+ end
774
+
775
+ def next_line
776
+ line = @template.shift || raise(StopIteration)
777
+
778
+ # `flat?' here is a little outdated,
779
+ # so we have to manually check if either the previous or current line
780
+ # closes the flat block, as well as whether a new block is opened.
781
+ line_defined = instance_variable_defined?(:@line)
782
+ @line.tabs if line_defined
783
+ unless (flat? && !closes_flat?(line) && !closes_flat?(@line)) ||
784
+ (line_defined && @line.text[0] == ?: && line.full =~ %r[^#{@line.full[/^\s+/]}\s])
785
+ return next_line if line.text.empty?
786
+
787
+ handle_multiline(line)
788
+ end
789
+
790
+ @next_line = line
791
+ end
792
+
793
+ def closes_flat?(line)
794
+ line && !line.text.empty? && line.full !~ /^#{@flat_spaces}/
795
+ end
796
+
797
+ def handle_multiline(line)
798
+ return unless is_multiline?(line.text)
799
+ line.text.slice!(-1)
800
+ loop do
801
+ new_line = @template.first
802
+ break if new_line.eod?
803
+ next @template.shift if new_line.text.strip.empty?
804
+ break unless is_multiline?(new_line.text.strip)
805
+ line.text << new_line.text.strip[0...-1]
806
+ @template.shift
807
+ end
808
+ end
809
+
810
+ # Checks whether or not `line` is in a multiline sequence.
811
+ def is_multiline?(text)
812
+ text && text.length > 1 && text[-1] == MULTILINE_CHAR_VALUE && text[-2] == ?\s && text !~ BLOCK_WITH_SPACES
813
+ end
814
+
815
+ def handle_ruby_multiline(line)
816
+ line.text.rstrip!
817
+ return line unless is_ruby_multiline?(line.text)
818
+ begin
819
+ # Use already fetched @next_line in the first loop. Otherwise, fetch next
820
+ new_line = new_line.nil? ? @next_line : @template.shift
821
+ break if new_line.eod?
822
+ next if new_line.text.empty?
823
+ line.text << " #{new_line.text.rstrip}"
824
+ end while is_ruby_multiline?(new_line.text)
825
+ next_line
826
+ line
827
+ end
828
+
829
+ # `text' is a Ruby multiline block if it:
830
+ # - ends with a comma
831
+ # - but not "?," which is a character literal
832
+ # (however, "x?," is a method call and not a literal)
833
+ # - and not "?\," which is a character literal
834
+ def is_ruby_multiline?(text)
835
+ text && text.length > 1 && text[-1] == ?, &&
836
+ !((text[-3, 2] =~ /\W\?/) || text[-3, 2] == "?\\")
837
+ end
838
+
839
+ def balance(*args)
840
+ Haml::Util.balance(*args) or raise(HamlSyntaxError.new(HamlError.message(:unbalanced_brackets)))
841
+ end
842
+
843
+ # Unlike #balance, this balances Ripper tokens to balance something like `{ a: "}" }` correctly.
844
+ def balance_tokens(buf, start, finish, count: 0)
845
+ text = ''.dup
846
+ Ripper.lex(buf).each do |_, token, str|
847
+ text << str
848
+ case token
849
+ when start
850
+ count += 1
851
+ when finish
852
+ count -= 1
853
+ end
854
+
855
+ if count == 0
856
+ return text, buf.sub(text, '')
857
+ end
858
+ end
859
+ raise HamlSyntaxError.new(HamlError.message(:unbalanced_brackets))
860
+ end
861
+
862
+ def block_opened?
863
+ @next_line.tabs > @line.tabs
864
+ end
865
+
866
+ # Same semantics as block_opened?, except that block_opened? uses Line#tabs,
867
+ # which doesn't interact well with filter lines
868
+ def filter_opened?
869
+ @next_line.full =~ (@indentation ? /^#{@indentation * (@template_tabs + 1)}/ : /^\s/)
870
+ end
871
+
872
+ def flat?
873
+ @flat
874
+ end
875
+
876
+ class << AttributeMerger = Object.new
877
+ # Merges two attribute hashes.
878
+ # This is the same as `to.merge!(from)`,
879
+ # except that it merges id, class, and data attributes.
880
+ #
881
+ # ids are concatenated with `"_"`,
882
+ # and classes are concatenated with `" "`.
883
+ # data hashes are simply merged.
884
+ #
885
+ # Destructively modifies `to`.
886
+ #
887
+ # @param to [{String => String,Hash}] The attribute hash to merge into
888
+ # @param from [{String => Object}] The attribute hash to merge from
889
+ # @return [{String => String,Hash}] `to`, after being merged
890
+ def merge_attributes!(to, from)
891
+ from.keys.each do |key|
892
+ to[key] = merge_value(key, to[key], from[key])
893
+ end
894
+ to
895
+ end
896
+
897
+ private
898
+
899
+ # @return [String, nil]
900
+ def filter_and_join(value, separator)
901
+ return '' if (value.respond_to?(:empty?) && value.empty?)
902
+
903
+ if value.is_a?(Array)
904
+ value = value.flatten
905
+ value.map! {|item| item ? item.to_s : nil}
906
+ value.compact!
907
+ value = value.join(separator)
908
+ else
909
+ value = value ? value.to_s : nil
910
+ end
911
+ !value.nil? && !value.empty? && value
912
+ end
913
+
914
+ # Merge a couple of values to one attribute value. No destructive operation.
915
+ #
916
+ # @param to [String,Hash,nil]
917
+ # @param from [Object]
918
+ # @return [String,Hash]
919
+ def merge_value(key, to, from)
920
+ if from.kind_of?(Hash) || to.kind_of?(Hash)
921
+ from = { nil => from } if !from.is_a?(Hash)
922
+ to = { nil => to } if !to.is_a?(Hash)
923
+ to.merge(from)
924
+ elsif key == 'id'
925
+ merged_id = filter_and_join(from, '_')
926
+ if to && merged_id
927
+ merged_id = "#{to}_#{merged_id}"
928
+ elsif to || merged_id
929
+ merged_id ||= to
930
+ end
931
+ merged_id
932
+ elsif key == 'class'
933
+ merged_class = filter_and_join(from, ' ')
934
+ if to && merged_class
935
+ merged_class = (to.split(' ') | merged_class.split(' ')).join(' ')
936
+ elsif to || merged_class
937
+ merged_class ||= to
938
+ end
939
+ merged_class
940
+ else
941
+ from
942
+ end
943
+ end
944
+ end
945
+ private_constant :AttributeMerger
946
+
947
+ class ParserOptions
948
+ # A list of options that are actually used in the parser
949
+ AVAILABLE_OPTIONS = %i[
950
+ autoclose
951
+ escape_html
952
+ filename
953
+ line
954
+ mime_type
955
+ preserve
956
+ remove_whitespace
957
+ suppress_eval
958
+ ].each do |option|
959
+ attr_reader option
960
+ end
961
+
962
+ DEFAULTS = {
963
+ autoclose: %w(area base basefont br col command embed frame
964
+ hr img input isindex keygen link menuitem meta
965
+ param source track wbr),
966
+ escape_html: false,
967
+ filename: '(haml)',
968
+ line: 1,
969
+ mime_type: 'text/html',
970
+ preserve: %w(textarea pre code),
971
+ remove_whitespace: false,
972
+ suppress_eval: false,
973
+ }
974
+
975
+ def initialize(values = {})
976
+ DEFAULTS.each {|k, v| instance_variable_set :"@#{k}", v}
977
+ AVAILABLE_OPTIONS.each do |key|
978
+ if values.key?(key)
979
+ instance_variable_set :"@#{key}", values[key]
980
+ end
981
+ end
982
+ end
983
+ end
984
+ private_constant :ParserOptions
985
+ end
986
+ end