haml 6.0.0.beta.1-java

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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