haml 4.0.7 → 5.2.2

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 (124) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/test.yml +36 -0
  3. data/.gitignore +19 -0
  4. data/.gitmodules +3 -0
  5. data/.yardopts +2 -3
  6. data/CHANGELOG.md +146 -4
  7. data/FAQ.md +4 -14
  8. data/Gemfile +16 -0
  9. data/MIT-LICENSE +2 -2
  10. data/README.md +90 -47
  11. data/REFERENCE.md +160 -74
  12. data/Rakefile +44 -63
  13. data/TODO +24 -0
  14. data/benchmark.rb +70 -0
  15. data/haml.gemspec +45 -0
  16. data/lib/haml/.gitattributes +1 -0
  17. data/lib/haml/attribute_builder.rb +219 -0
  18. data/lib/haml/attribute_compiler.rb +237 -0
  19. data/lib/haml/attribute_parser.rb +150 -0
  20. data/lib/haml/buffer.rb +12 -175
  21. data/lib/haml/compiler.rb +110 -320
  22. data/lib/haml/engine.rb +34 -41
  23. data/lib/haml/error.rb +28 -24
  24. data/lib/haml/escapable.rb +77 -0
  25. data/lib/haml/exec.rb +38 -20
  26. data/lib/haml/filters.rb +22 -27
  27. data/lib/haml/generator.rb +42 -0
  28. data/lib/haml/helpers/action_view_extensions.rb +4 -2
  29. data/lib/haml/helpers/action_view_mods.rb +45 -60
  30. data/lib/haml/helpers/action_view_xss_mods.rb +2 -0
  31. data/lib/haml/helpers/safe_erubi_template.rb +20 -0
  32. data/lib/haml/helpers/safe_erubis_template.rb +5 -1
  33. data/lib/haml/helpers/xss_mods.rb +23 -13
  34. data/lib/haml/helpers.rb +134 -89
  35. data/lib/haml/options.rb +63 -69
  36. data/lib/haml/parser.rb +319 -227
  37. data/lib/haml/plugin.rb +54 -0
  38. data/lib/haml/railtie.rb +43 -12
  39. data/lib/haml/sass_rails_filter.rb +18 -4
  40. data/lib/haml/template/options.rb +13 -2
  41. data/lib/haml/template.rb +13 -6
  42. data/lib/haml/temple_engine.rb +124 -0
  43. data/lib/haml/temple_line_counter.rb +30 -0
  44. data/lib/haml/util.rb +83 -202
  45. data/lib/haml/version.rb +3 -1
  46. data/lib/haml.rb +2 -0
  47. data/yard/default/.gitignore +1 -0
  48. data/yard/default/fulldoc/html/css/common.sass +15 -0
  49. data/yard/default/layout/html/footer.erb +12 -0
  50. metadata +73 -115
  51. data/lib/haml/template/plugin.rb +0 -41
  52. data/test/engine_test.rb +0 -2013
  53. data/test/erb/_av_partial_1.erb +0 -12
  54. data/test/erb/_av_partial_2.erb +0 -8
  55. data/test/erb/action_view.erb +0 -62
  56. data/test/erb/standard.erb +0 -55
  57. data/test/filters_test.rb +0 -254
  58. data/test/gemfiles/Gemfile.rails-3.0.x +0 -5
  59. data/test/gemfiles/Gemfile.rails-3.1.x +0 -6
  60. data/test/gemfiles/Gemfile.rails-3.2.x +0 -5
  61. data/test/gemfiles/Gemfile.rails-4.0.x +0 -5
  62. data/test/haml-spec/LICENSE +0 -14
  63. data/test/haml-spec/README.md +0 -106
  64. data/test/haml-spec/lua_haml_spec.lua +0 -38
  65. data/test/haml-spec/perl_haml_test.pl +0 -81
  66. data/test/haml-spec/ruby_haml_test.rb +0 -23
  67. data/test/haml-spec/tests.json +0 -660
  68. data/test/helper_test.rb +0 -583
  69. data/test/markaby/standard.mab +0 -52
  70. data/test/mocks/article.rb +0 -6
  71. data/test/parser_test.rb +0 -105
  72. data/test/results/content_for_layout.xhtml +0 -12
  73. data/test/results/eval_suppressed.xhtml +0 -9
  74. data/test/results/helpers.xhtml +0 -70
  75. data/test/results/helpful.xhtml +0 -10
  76. data/test/results/just_stuff.xhtml +0 -70
  77. data/test/results/list.xhtml +0 -12
  78. data/test/results/nuke_inner_whitespace.xhtml +0 -40
  79. data/test/results/nuke_outer_whitespace.xhtml +0 -148
  80. data/test/results/original_engine.xhtml +0 -20
  81. data/test/results/partial_layout.xhtml +0 -5
  82. data/test/results/partial_layout_erb.xhtml +0 -5
  83. data/test/results/partials.xhtml +0 -21
  84. data/test/results/render_layout.xhtml +0 -3
  85. data/test/results/silent_script.xhtml +0 -74
  86. data/test/results/standard.xhtml +0 -162
  87. data/test/results/tag_parsing.xhtml +0 -23
  88. data/test/results/very_basic.xhtml +0 -5
  89. data/test/results/whitespace_handling.xhtml +0 -90
  90. data/test/template_test.rb +0 -354
  91. data/test/templates/_av_partial_1.haml +0 -9
  92. data/test/templates/_av_partial_1_ugly.haml +0 -9
  93. data/test/templates/_av_partial_2.haml +0 -5
  94. data/test/templates/_av_partial_2_ugly.haml +0 -5
  95. data/test/templates/_layout.erb +0 -3
  96. data/test/templates/_layout_for_partial.haml +0 -3
  97. data/test/templates/_partial.haml +0 -8
  98. data/test/templates/_text_area.haml +0 -3
  99. data/test/templates/_text_area_helper.html.haml +0 -4
  100. data/test/templates/action_view.haml +0 -47
  101. data/test/templates/action_view_ugly.haml +0 -47
  102. data/test/templates/breakage.haml +0 -8
  103. data/test/templates/content_for_layout.haml +0 -8
  104. data/test/templates/eval_suppressed.haml +0 -11
  105. data/test/templates/helpers.haml +0 -55
  106. data/test/templates/helpful.haml +0 -11
  107. data/test/templates/just_stuff.haml +0 -85
  108. data/test/templates/list.haml +0 -12
  109. data/test/templates/nuke_inner_whitespace.haml +0 -32
  110. data/test/templates/nuke_outer_whitespace.haml +0 -144
  111. data/test/templates/original_engine.haml +0 -17
  112. data/test/templates/partial_layout.haml +0 -3
  113. data/test/templates/partial_layout_erb.erb +0 -4
  114. data/test/templates/partialize.haml +0 -1
  115. data/test/templates/partials.haml +0 -12
  116. data/test/templates/render_layout.haml +0 -2
  117. data/test/templates/silent_script.haml +0 -45
  118. data/test/templates/standard.haml +0 -43
  119. data/test/templates/standard_ugly.haml +0 -43
  120. data/test/templates/tag_parsing.haml +0 -21
  121. data/test/templates/very_basic.haml +0 -4
  122. data/test/templates/whitespace_handling.haml +0 -87
  123. data/test/test_helper.rb +0 -81
  124. data/test/util_test.rb +0 -63
data/lib/haml/parser.rb CHANGED
@@ -1,3 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ripper'
1
4
  require 'strscan'
2
5
 
3
6
  module Haml
@@ -59,7 +62,7 @@ module Haml
59
62
  SILENT_SCRIPT,
60
63
  ESCAPE,
61
64
  FILTER
62
- ]
65
+ ].freeze
63
66
 
64
67
  # The value of the character that designates that a line is part
65
68
  # of a multiline string.
@@ -71,26 +74,28 @@ module Haml
71
74
  # foo.each do | bar |
72
75
  # = bar
73
76
  #
74
- BLOCK_WITH_SPACES = /do[\s]*\|[\s]*[^\|]*[\s]+\|\z/
77
+ BLOCK_WITH_SPACES = /do\s*\|\s*[^\|]*\s+\|\z/
75
78
 
76
- MID_BLOCK_KEYWORDS = %w[else elsif rescue ensure end when]
77
- START_BLOCK_KEYWORDS = %w[if begin case unless]
79
+ MID_BLOCK_KEYWORDS = %w[else elsif rescue ensure end when].freeze
80
+ START_BLOCK_KEYWORDS = %w[if begin case unless].freeze
78
81
  # Try to parse assignments to block starters as best as possible
79
82
  START_BLOCK_KEYWORD_REGEX = /(?:\w+(?:,\s*\w+)*\s*=\s*)?(#{START_BLOCK_KEYWORDS.join('|')})/
80
83
  BLOCK_KEYWORD_REGEX = /^-?\s*(?:(#{MID_BLOCK_KEYWORDS.join('|')})|#{START_BLOCK_KEYWORD_REGEX.source})\b/
81
84
 
82
85
  # The Regex that matches a Doctype command.
83
- DOCTYPE_REGEX = /(\d(?:\.\d)?)?[\s]*([a-z]*)\s*([^ ]+)?/i
86
+ DOCTYPE_REGEX = /(\d(?:\.\d)?)?\s*([a-z]*)\s*([^ ]+)?/i
84
87
 
85
88
  # The Regex that matches a literal string or symbol value
86
89
  LITERAL_VALUE_REGEX = /:(\w*)|(["'])((?!\\|\#\{|\#@|\#\$|\2).|\\.)*\2/
87
90
 
88
- def initialize(template, options)
89
- # :eod is a special end-of-document marker
90
- @template = (template.rstrip).split(/\r\n|\r|\n/) + [:eod, :eod]
91
- @options = options
92
- @flat = false
93
- @index = 0
91
+ ID_KEY = 'id'.freeze
92
+ CLASS_KEY = 'class'.freeze
93
+
94
+ # Used for scanning old attributes, substituting the first '{'
95
+ METHOD_CALL_PREFIX = 'a('
96
+
97
+ def initialize(options)
98
+ @options = Options.wrap(options)
94
99
  # Record the indent levels of "if" statements to validate the subsequent
95
100
  # elsif and else statements are indented at the appropriate level.
96
101
  @script_level_stack = []
@@ -98,15 +103,27 @@ module Haml
98
103
  @template_tabs = 0
99
104
  end
100
105
 
101
- def parse
106
+ def call(template)
107
+ match = template.rstrip.scan(/(([ \t]+)?(.*?))(?:\Z|\r\n|\r|\n)/m)
108
+ # discard the last match which is always blank
109
+ match.pop
110
+ @template = match.each_with_index.map do |(full, whitespace, text), index|
111
+ Line.new(whitespace, text.rstrip, full, index, self, false)
112
+ end
113
+ # Append special end-of-document marker
114
+ @template << Line.new(nil, '-#', '-#', @template.size, self, true)
115
+
102
116
  @root = @parent = ParseNode.new(:root)
103
- @haml_comment = false
117
+ @flat = false
118
+ @filter_buffer = nil
104
119
  @indentation = nil
105
120
  @line = next_line
106
121
 
107
122
  raise SyntaxError.new(Error.message(:indenting_at_start), @line.index) if @line.tabs != 0
108
123
 
109
- while next_line
124
+ loop do
125
+ next_line
126
+
110
127
  process_indent(@line) unless @line.text.empty?
111
128
 
112
129
  if flat?
@@ -118,75 +135,103 @@ module Haml
118
135
  end
119
136
 
120
137
  @tab_up = nil
121
- process_line(@line.text, @line.index) unless @line.text.empty? || @haml_comment
122
- if @parent.type != :haml_comment && (block_opened? || @tab_up)
138
+ process_line(@line) unless @line.text.empty?
139
+ if block_opened? || @tab_up
123
140
  @template_tabs += 1
124
141
  @parent = @parent.children.last
125
142
  end
126
143
 
127
- if !@haml_comment && !flat? && @next_line.tabs - @line.tabs > 1
144
+ if !flat? && @next_line.tabs - @line.tabs > 1
128
145
  raise SyntaxError.new(Error.message(:deeper_indenting, @next_line.tabs - @line.tabs), @next_line.index)
129
146
  end
130
147
 
131
148
  @line = @next_line
132
149
  end
133
-
134
150
  # Close all the open tags
135
151
  close until @parent.type == :root
136
152
  @root
137
153
  rescue Haml::Error => e
138
- e.backtrace.unshift "#{@options[:filename]}:#{(e.line ? e.line + 1 : @index) + @options[:line] - 1}"
154
+ e.backtrace.unshift "#{@options.filename}:#{(e.line ? e.line + 1 : @line.index + 1) + @options.line - 1}"
139
155
  raise
140
156
  end
141
157
 
158
+ def compute_tabs(line)
159
+ return 0 if line.text.empty? || !line.whitespace
160
+
161
+ if @indentation.nil?
162
+ @indentation = line.whitespace
163
+
164
+ if @indentation.include?(?\s) && @indentation.include?(?\t)
165
+ raise SyntaxError.new(Error.message(:cant_use_tabs_and_spaces), line.index)
166
+ end
167
+
168
+ @flat_spaces = @indentation * (@template_tabs+1) if flat?
169
+ return 1
170
+ end
171
+
172
+ tabs = line.whitespace.length / @indentation.length
173
+ return tabs if line.whitespace == @indentation * tabs
174
+ return @template_tabs + 1 if flat? && line.whitespace =~ /^#{@flat_spaces}/
175
+
176
+ message = Error.message(:inconsistent_indentation,
177
+ human_indentation(line.whitespace),
178
+ human_indentation(@indentation)
179
+ )
180
+ raise SyntaxError.new(message, line.index)
181
+ end
142
182
 
143
183
  private
144
184
 
145
185
  # @private
146
- class Line < Struct.new(:text, :unstripped, :full, :index, :compiler, :eod)
186
+ Line = Struct.new(:whitespace, :text, :full, :index, :parser, :eod) do
147
187
  alias_method :eod?, :eod
148
188
 
149
189
  # @private
150
190
  def tabs
151
- line = self
152
- @tabs ||= compiler.instance_eval do
153
- break 0 if line.text.empty? || !(whitespace = line.full[/^\s+/])
154
-
155
- if @indentation.nil?
156
- @indentation = whitespace
157
-
158
- if @indentation.include?(?\s) && @indentation.include?(?\t)
159
- raise SyntaxError.new(Error.message(:cant_use_tabs_and_spaces), line.index)
160
- end
161
-
162
- @flat_spaces = @indentation * (@template_tabs+1) if flat?
163
- break 1
164
- end
165
-
166
- tabs = whitespace.length / @indentation.length
167
- break tabs if whitespace == @indentation * tabs
168
- break @template_tabs + 1 if flat? && whitespace =~ /^#{@flat_spaces}/
191
+ @tabs ||= parser.compute_tabs(self)
192
+ end
169
193
 
170
- message = Error.message(:inconsistent_indentation,
171
- Haml::Util.human_indentation(whitespace),
172
- Haml::Util.human_indentation(@indentation)
173
- )
174
- raise SyntaxError.new(message, line.index)
175
- end
194
+ def strip!(from)
195
+ self.text = text[from..-1]
196
+ self.text.lstrip!
197
+ self
176
198
  end
177
199
  end
178
200
 
179
201
  # @private
180
- class ParseNode < Struct.new(:type, :line, :value, :parent, :children)
202
+ ParseNode = Struct.new(:type, :line, :value, :parent, :children) do
181
203
  def initialize(*args)
182
204
  super
183
205
  self.children ||= []
184
206
  end
185
207
 
186
208
  def inspect
187
- text = "(#{type} #{value.inspect}"
188
- children.each {|c| text << "\n" << c.inspect.gsub(/^/, " ")}
189
- text + ")"
209
+ %Q[(#{type} #{value.inspect}#{children.each_with_object(''.dup) {|c, s| s << "\n#{c.inspect.gsub!(/^/, ' ')}"}})].dup
210
+ end
211
+ end
212
+
213
+ # @param [String] new - Hash literal including dynamic values.
214
+ # @param [String] old - Hash literal including dynamic values or Ruby literal of multiple Hashes which MUST be interpreted as method's last arguments.
215
+ DynamicAttributes = Struct.new(:new, :old) do
216
+ undef :old=
217
+ def old=(value)
218
+ unless value =~ /\A{.*}\z/m
219
+ raise ArgumentError.new('Old attributes must start with "{" and end with "}"')
220
+ end
221
+ self[:old] = value
222
+ end
223
+
224
+ # This will be a literal for Haml::Buffer#attributes's last argument, `attributes_hashes`.
225
+ def to_literal
226
+ [new, stripped_old].compact.join(', ')
227
+ end
228
+
229
+ private
230
+
231
+ # For `%foo{ { foo: 1 }, bar: 2 }`, :old is "{ { foo: 1 }, bar: 2 }" and this method returns " { foo: 1 }, bar: 2 " for last argument.
232
+ def stripped_old
233
+ return nil if old.nil?
234
+ old.sub!(/\A{/, '').sub!(/}\z/m, '')
190
235
  end
191
236
  end
192
237
 
@@ -195,98 +240,104 @@ module Haml
195
240
  return unless line.tabs <= @template_tabs && @template_tabs > 0
196
241
 
197
242
  to_close = @template_tabs - line.tabs
198
- to_close.times {|i| close unless to_close - 1 - i == 0 && mid_block_keyword?(line.text)}
243
+ to_close.times {|i| close unless to_close - 1 - i == 0 && continuation_script?(line.text)}
244
+ end
245
+
246
+ def continuation_script?(text)
247
+ text[0] == SILENT_SCRIPT && mid_block_keyword?(text)
248
+ end
249
+
250
+ def mid_block_keyword?(text)
251
+ MID_BLOCK_KEYWORDS.include?(block_keyword(text))
199
252
  end
200
253
 
201
254
  # Processes a single line of Haml.
202
255
  #
203
256
  # This method doesn't return anything; it simply processes the line and
204
257
  # adds the appropriate code to `@precompiled`.
205
- def process_line(text, index)
206
- @index = index + 1
207
-
208
- case text[0]
209
- when DIV_CLASS; push div(text)
258
+ def process_line(line)
259
+ case line.text[0]
260
+ when DIV_CLASS; push div(line)
210
261
  when DIV_ID
211
- return push plain(text) if text[1] == ?{
212
- push div(text)
213
- when ELEMENT; push tag(text)
214
- when COMMENT; push comment(text[1..-1].strip)
262
+ return push plain(line) if %w[{ @ $].include?(line.text[1])
263
+ push div(line)
264
+ when ELEMENT; push tag(line)
265
+ when COMMENT; push comment(line.text[1..-1].lstrip)
215
266
  when SANITIZE
216
- return push plain(text[3..-1].strip, :escape_html) if text[1..2] == "=="
217
- return push script(text[2..-1].strip, :escape_html) if text[1] == SCRIPT
218
- return push flat_script(text[2..-1].strip, :escape_html) if text[1] == FLAT_SCRIPT
219
- return push plain(text[1..-1].strip, :escape_html) if text[1] == ?\s
220
- push plain(text)
267
+ return push plain(line.strip!(3), :escape_html) if line.text[1, 2] == '=='
268
+ return push script(line.strip!(2), :escape_html) if line.text[1] == SCRIPT
269
+ return push flat_script(line.strip!(2), :escape_html) if line.text[1] == FLAT_SCRIPT
270
+ return push plain(line.strip!(1), :escape_html) if line.text[1] == ?\s || line.text[1..2] == '#{'
271
+ push plain(line)
221
272
  when SCRIPT
222
- return push plain(text[2..-1].strip) if text[1] == SCRIPT
223
- push script(text[1..-1])
224
- when FLAT_SCRIPT; push flat_script(text[1..-1])
225
- when SILENT_SCRIPT; push silent_script(text)
226
- when FILTER; push filter(text[1..-1].downcase)
273
+ return push plain(line.strip!(2)) if line.text[1] == SCRIPT
274
+ line.text = line.text[1..-1]
275
+ push script(line)
276
+ when FLAT_SCRIPT; push flat_script(line.strip!(1))
277
+ when SILENT_SCRIPT
278
+ return push haml_comment(line.text[2..-1]) if line.text[1] == SILENT_COMMENT
279
+ push silent_script(line)
280
+ when FILTER; push filter(line.text[1..-1].downcase)
227
281
  when DOCTYPE
228
- return push doctype(text) if text[0...3] == '!!!'
229
- return push plain(text[3..-1].strip, false) if text[1..2] == "=="
230
- return push script(text[2..-1].strip, false) if text[1] == SCRIPT
231
- return push flat_script(text[2..-1].strip, false) if text[1] == FLAT_SCRIPT
232
- return push plain(text[1..-1].strip, false) if text[1] == ?\s
233
- push plain(text)
234
- when ESCAPE; push plain(text[1..-1])
235
- else; push plain(text)
282
+ return push doctype(line.text) if line.text[0, 3] == '!!!'
283
+ return push plain(line.strip!(3), false) if line.text[1, 2] == '=='
284
+ return push script(line.strip!(2), false) if line.text[1] == SCRIPT
285
+ return push flat_script(line.strip!(2), false) if line.text[1] == FLAT_SCRIPT
286
+ return push plain(line.strip!(1), false) if line.text[1] == ?\s || line.text[1..2] == '#{'
287
+ push plain(line)
288
+ when ESCAPE
289
+ line.text = line.text[1..-1]
290
+ push plain(line)
291
+ else; push plain(line)
236
292
  end
237
293
  end
238
294
 
239
295
  def block_keyword(text)
240
- return unless keyword = text.scan(BLOCK_KEYWORD_REGEX)[0]
296
+ return unless (keyword = text.scan(BLOCK_KEYWORD_REGEX)[0])
241
297
  keyword[0] || keyword[1]
242
298
  end
243
299
 
244
- def mid_block_keyword?(text)
245
- MID_BLOCK_KEYWORDS.include?(block_keyword(text))
246
- end
247
-
248
300
  def push(node)
249
301
  @parent.children << node
250
302
  node.parent = @parent
251
303
  end
252
304
 
253
- def plain(text, escape_html = nil)
305
+ def plain(line, escape_html = nil)
254
306
  if block_opened?
255
307
  raise SyntaxError.new(Error.message(:illegal_nesting_plain), @next_line.index)
256
308
  end
257
309
 
258
- unless contains_interpolation?(text)
259
- return ParseNode.new(:plain, @index, :text => text)
310
+ unless contains_interpolation?(line.text)
311
+ return ParseNode.new(:plain, line.index + 1, :text => line.text)
260
312
  end
261
313
 
262
- escape_html = @options[:escape_html] if escape_html.nil?
263
- script(unescape_interpolation(text, escape_html), false)
314
+ escape_html = @options.escape_html && @options.mime_type != 'text/plain' if escape_html.nil?
315
+ line.text = unescape_interpolation(line.text, escape_html)
316
+ script(line, false)
264
317
  end
265
318
 
266
- def script(text, escape_html = nil, preserve = false)
267
- raise SyntaxError.new(Error.message(:no_ruby_code, '=')) if text.empty?
268
- text = handle_ruby_multiline(text)
269
- escape_html = @options[:escape_html] if escape_html.nil?
319
+ def script(line, escape_html = nil, preserve = false)
320
+ raise SyntaxError.new(Error.message(:no_ruby_code, '=')) if line.text.empty?
321
+ line = handle_ruby_multiline(line)
322
+ escape_html = @options.escape_html if escape_html.nil?
270
323
 
271
- keyword = block_keyword(text)
324
+ keyword = block_keyword(line.text)
272
325
  check_push_script_stack(keyword)
273
326
 
274
- ParseNode.new(:script, @index, :text => text, :escape_html => escape_html,
327
+ ParseNode.new(:script, line.index + 1, :text => line.text, :escape_html => escape_html,
275
328
  :preserve => preserve, :keyword => keyword)
276
329
  end
277
330
 
278
- def flat_script(text, escape_html = nil)
279
- raise SyntaxError.new(Error.message(:no_ruby_code, '~')) if text.empty?
280
- script(text, escape_html, :preserve)
331
+ def flat_script(line, escape_html = nil)
332
+ raise SyntaxError.new(Error.message(:no_ruby_code, '~')) if line.text.empty?
333
+ script(line, escape_html, :preserve)
281
334
  end
282
335
 
283
- def silent_script(text)
284
- return haml_comment(text[2..-1]) if text[1] == SILENT_COMMENT
285
-
286
- raise SyntaxError.new(Error.message(:no_end), @index - 1) if text[1..-1].strip == "end"
336
+ def silent_script(line)
337
+ raise SyntaxError.new(Error.message(:no_end), line.index) if line.text[1..-1].strip == 'end'
287
338
 
288
- text = handle_ruby_multiline(text)
289
- keyword = block_keyword(text)
339
+ line = handle_ruby_multiline(line)
340
+ keyword = block_keyword(line.text)
290
341
 
291
342
  check_push_script_stack(keyword)
292
343
 
@@ -308,8 +359,8 @@ module Haml
308
359
  end
309
360
  end
310
361
 
311
- ParseNode.new(:silent_script, @index,
312
- :text => text[1..-1], :keyword => keyword)
362
+ ParseNode.new(:silent_script, @line.index + 1,
363
+ :text => line.text[1..-1], :keyword => keyword)
313
364
  end
314
365
 
315
366
  def check_push_script_stack(keyword)
@@ -323,18 +374,25 @@ module Haml
323
374
  end
324
375
 
325
376
  def haml_comment(text)
326
- @haml_comment = block_opened?
327
- ParseNode.new(:haml_comment, @index, :text => text)
377
+ if filter_opened?
378
+ @flat = true
379
+ @filter_buffer = String.new
380
+ @filter_buffer << "#{text}\n" unless text.empty?
381
+ text = @filter_buffer
382
+ # If we don't know the indentation by now, it'll be set in Line#tabs
383
+ @flat_spaces = @indentation * (@template_tabs+1) if @indentation
384
+ end
385
+
386
+ ParseNode.new(:haml_comment, @line.index + 1, :text => text)
328
387
  end
329
388
 
330
389
  def tag(line)
331
390
  tag_name, attributes, attributes_hashes, object_ref, nuke_outer_whitespace,
332
- nuke_inner_whitespace, action, value, last_line = parse_tag(line)
391
+ nuke_inner_whitespace, action, value, last_line = parse_tag(line.text)
333
392
 
334
- preserve_tag = @options[:preserve].include?(tag_name)
393
+ preserve_tag = @options.preserve.include?(tag_name)
335
394
  nuke_inner_whitespace ||= preserve_tag
336
- preserve_tag = false if @options[:ugly]
337
- escape_html = (action == '&' || (action != '!' && @options[:escape_html]))
395
+ escape_html = (action == '&' || (action != '!' && @options.escape_html))
338
396
 
339
397
  case action
340
398
  when '/'; self_closing = true
@@ -369,22 +427,20 @@ module Haml
369
427
  end
370
428
 
371
429
  attributes = Parser.parse_class_and_id(attributes)
372
- attributes_list = []
430
+ dynamic_attributes = DynamicAttributes.new
373
431
 
374
432
  if attributes_hashes[:new]
375
433
  static_attributes, attributes_hash = attributes_hashes[:new]
376
- Buffer.merge_attrs(attributes, static_attributes) if static_attributes
377
- attributes_list << attributes_hash
434
+ AttributeBuilder.merge_attributes!(attributes, static_attributes) if static_attributes
435
+ dynamic_attributes.new = attributes_hash
378
436
  end
379
437
 
380
438
  if attributes_hashes[:old]
381
439
  static_attributes = parse_static_hash(attributes_hashes[:old])
382
- Buffer.merge_attrs(attributes, static_attributes) if static_attributes
383
- attributes_list << attributes_hashes[:old] unless static_attributes || @options[:suppress_eval]
440
+ AttributeBuilder.merge_attributes!(attributes, static_attributes) if static_attributes
441
+ dynamic_attributes.old = attributes_hashes[:old] unless static_attributes || @options.suppress_eval
384
442
  end
385
443
 
386
- attributes_list.compact!
387
-
388
444
  raise SyntaxError.new(Error.message(:illegal_nesting_self_closing), @next_line.index) if block_opened? && self_closing
389
445
  raise SyntaxError.new(Error.message(:no_ruby_code, action), last_line - 1) if parse && value.empty?
390
446
  raise SyntaxError.new(Error.message(:self_closing_content), last_line - 1) if self_closing && !value.empty?
@@ -393,56 +449,70 @@ module Haml
393
449
  raise SyntaxError.new(Error.message(:illegal_nesting_line, tag_name), @next_line.index)
394
450
  end
395
451
 
396
- self_closing ||= !!(!block_opened? && value.empty? && @options[:autoclose].any? {|t| t === tag_name})
452
+ self_closing ||= !!(!block_opened? && value.empty? && @options.autoclose.any? {|t| t === tag_name})
397
453
  value = nil if value.empty? && (block_opened? || self_closing)
398
- value = handle_ruby_multiline(value) if parse
454
+ line.text = value
455
+ line = handle_ruby_multiline(line) if parse
399
456
 
400
- ParseNode.new(:tag, @index, :name => tag_name, :attributes => attributes,
401
- :attributes_hashes => attributes_list, :self_closing => self_closing,
457
+ ParseNode.new(:tag, line.index + 1, :name => tag_name, :attributes => attributes,
458
+ :dynamic_attributes => dynamic_attributes, :self_closing => self_closing,
402
459
  :nuke_inner_whitespace => nuke_inner_whitespace,
403
460
  :nuke_outer_whitespace => nuke_outer_whitespace, :object_ref => object_ref,
404
461
  :escape_html => escape_html, :preserve_tag => preserve_tag,
405
- :preserve_script => preserve_script, :parse => parse, :value => value)
462
+ :preserve_script => preserve_script, :parse => parse, :value => line.text)
406
463
  end
407
464
 
408
465
  # Renders a line that creates an XHTML tag and has an implicit div because of
409
466
  # `.` or `#`.
410
467
  def div(line)
411
- tag('%div' + line)
468
+ line.text = "%div#{line.text}"
469
+ tag(line)
412
470
  end
413
471
 
414
472
  # Renders an XHTML comment.
415
- def comment(line)
416
- conditional, line = balance(line, ?[, ?]) if line[0] == ?[
417
- line.strip!
418
- conditional << ">" if conditional
473
+ def comment(text)
474
+ if text[0..1] == '!['
475
+ revealed = true
476
+ text = text[1..-1]
477
+ else
478
+ revealed = false
479
+ end
419
480
 
420
- if block_opened? && !line.empty?
481
+ conditional, text = balance(text, ?[, ?]) if text[0] == ?[
482
+ text.strip!
483
+
484
+ if contains_interpolation?(text)
485
+ parse = true
486
+ text = unescape_interpolation(text)
487
+ else
488
+ parse = false
489
+ end
490
+
491
+ if block_opened? && !text.empty?
421
492
  raise SyntaxError.new(Haml::Error.message(:illegal_nesting_content), @next_line.index)
422
493
  end
423
494
 
424
- ParseNode.new(:comment, @index, :conditional => conditional, :text => line)
495
+ ParseNode.new(:comment, @line.index + 1, :conditional => conditional, :text => text, :revealed => revealed, :parse => parse)
425
496
  end
426
497
 
427
498
  # Renders an XHTML doctype or XML shebang.
428
- def doctype(line)
499
+ def doctype(text)
429
500
  raise SyntaxError.new(Error.message(:illegal_nesting_header), @next_line.index) if block_opened?
430
- version, type, encoding = line[3..-1].strip.downcase.scan(DOCTYPE_REGEX)[0]
431
- ParseNode.new(:doctype, @index, :version => version, :type => type, :encoding => encoding)
501
+ version, type, encoding = text[3..-1].strip.downcase.scan(DOCTYPE_REGEX)[0]
502
+ ParseNode.new(:doctype, @line.index + 1, :version => version, :type => type, :encoding => encoding)
432
503
  end
433
504
 
434
505
  def filter(name)
435
506
  raise Error.new(Error.message(:invalid_filter_name, name)) unless name =~ /^\w+$/
436
507
 
437
- @filter_buffer = String.new
438
-
439
508
  if filter_opened?
440
509
  @flat = true
510
+ @filter_buffer = String.new
441
511
  # If we don't know the indentation by now, it'll be set in Line#tabs
442
512
  @flat_spaces = @indentation * (@template_tabs+1) if @indentation
443
513
  end
444
514
 
445
- ParseNode.new(:filter, @index, :name => name, :text => @filter_buffer)
515
+ ParseNode.new(:filter, @line.index + 1, :name => name, :text => @filter_buffer)
446
516
  end
447
517
 
448
518
  def close
@@ -452,13 +522,17 @@ module Haml
452
522
  end
453
523
 
454
524
  def close_filter(_)
455
- @flat = false
456
- @flat_spaces = nil
457
- @filter_buffer = nil
525
+ close_flat_section
458
526
  end
459
527
 
460
528
  def close_haml_comment(_)
461
- @haml_comment = false
529
+ close_flat_section
530
+ end
531
+
532
+ def close_flat_section
533
+ @flat = false
534
+ @flat_spaces = nil
535
+ @filter_buffer = nil
462
536
  end
463
537
 
464
538
  def close_silent_script(node)
@@ -466,7 +540,7 @@ module Haml
466
540
 
467
541
  # Post-process case statements to normalize the nesting of "when" clauses
468
542
  return unless node.value[:keyword] == "case"
469
- return unless first = node.children.first
543
+ return unless (first = node.children.first)
470
544
  return unless first.type == :silent_script && first.value[:keyword] == "when"
471
545
  return if first.children.empty?
472
546
  # If the case node has a "when" child with children, it's the
@@ -485,29 +559,39 @@ module Haml
485
559
  # that can then be merged with another attributes hash.
486
560
  def self.parse_class_and_id(list)
487
561
  attributes = {}
488
- list.scan(/([#.])([-:_a-zA-Z0-9]+)/) do |type, property|
562
+ return attributes if list.empty?
563
+
564
+ list.scan(/([#.])([-:_a-zA-Z0-9\@]+)/) do |type, property|
489
565
  case type
490
566
  when '.'
491
- if attributes['class']
492
- attributes['class'] += " "
567
+ if attributes[CLASS_KEY]
568
+ attributes[CLASS_KEY] += " "
493
569
  else
494
- attributes['class'] = ""
570
+ attributes[CLASS_KEY] = ""
495
571
  end
496
- attributes['class'] += property
497
- when '#'; attributes['id'] = property
572
+ attributes[CLASS_KEY] += property
573
+ when '#'; attributes[ID_KEY] = property
498
574
  end
499
575
  end
500
576
  attributes
501
577
  end
502
578
 
579
+ # This method doesn't use Haml::AttributeParser because currently it depends on Ripper and Rubinius doesn't provide it.
580
+ # Ideally this logic should be placed in Haml::AttributeParser instead of here and this method should use it.
581
+ #
582
+ # @param [String] text - Hash literal or text inside old attributes
583
+ # @return [Hash,nil] - Return nil if text is not static Hash literal
503
584
  def parse_static_hash(text)
504
585
  attributes = {}
586
+ return attributes if text.empty?
587
+
588
+ text = text[1...-1] # strip brackets
505
589
  scanner = StringScanner.new(text)
506
590
  scanner.scan(/\s+/)
507
591
  until scanner.eos?
508
- return unless key = scanner.scan(LITERAL_VALUE_REGEX)
592
+ return unless (key = scanner.scan(LITERAL_VALUE_REGEX))
509
593
  return unless scanner.scan(/\s*=>\s*/)
510
- return unless value = scanner.scan(LITERAL_VALUE_REGEX)
594
+ return unless (value = scanner.scan(LITERAL_VALUE_REGEX))
511
595
  return unless scanner.scan(/\s*(?:,|$)\s*/)
512
596
  attributes[eval(key).to_s] = eval(value).to_s
513
597
  end
@@ -515,20 +599,20 @@ module Haml
515
599
  end
516
600
 
517
601
  # Parses a line into tag_name, attributes, attributes_hash, object_ref, action, value
518
- def parse_tag(line)
519
- match = line.scan(/%([-:\w]+)([-:\w\.\#]*)(.*)/)[0]
520
- raise SyntaxError.new(Error.message(:invalid_tag, line)) unless match
602
+ def parse_tag(text)
603
+ match = text.scan(/%([-:\w]+)([-:\w.#\@]*)(.+)?/)[0]
604
+ raise SyntaxError.new(Error.message(:invalid_tag, text)) unless match
521
605
 
522
606
  tag_name, attributes, rest = match
523
607
 
524
- if attributes =~ /[\.#](\.|#|\z)/
608
+ if !attributes.empty? && (attributes =~ /[.#](\.|#|\z)/)
525
609
  raise SyntaxError.new(Error.message(:illegal_element))
526
610
  end
527
611
 
528
612
  new_attributes_hash = old_attributes_hash = last_line = nil
529
- object_ref = "nil"
613
+ object_ref = :nil
530
614
  attributes_hashes = {}
531
- while rest
615
+ while rest && !rest.empty?
532
616
  case rest[0]
533
617
  when ?{
534
618
  break if old_attributes_hash
@@ -539,38 +623,51 @@ module Haml
539
623
  new_attributes_hash, rest, last_line = parse_new_attributes(rest)
540
624
  attributes_hashes[:new] = new_attributes_hash
541
625
  when ?[
542
- break unless object_ref == "nil"
626
+ break unless object_ref == :nil
543
627
  object_ref, rest = balance(rest, ?[, ?])
544
628
  else; break
545
629
  end
546
630
  end
547
631
 
548
- if rest
632
+ if rest && !rest.empty?
549
633
  nuke_whitespace, action, value = rest.scan(/(<>|><|[><])?([=\/\~&!])?(.*)?/)[0]
550
- nuke_whitespace ||= ''
551
- nuke_outer_whitespace = nuke_whitespace.include? '>'
552
- nuke_inner_whitespace = nuke_whitespace.include? '<'
634
+ if nuke_whitespace
635
+ nuke_outer_whitespace = nuke_whitespace.include? '>'
636
+ nuke_inner_whitespace = nuke_whitespace.include? '<'
637
+ end
553
638
  end
554
639
 
555
- if @options[:remove_whitespace]
640
+ if @options.remove_whitespace
556
641
  nuke_outer_whitespace = true
557
642
  nuke_inner_whitespace = true
558
643
  end
559
644
 
560
- value = value.to_s.strip
645
+ if value.nil?
646
+ value = ''
647
+ else
648
+ value.strip!
649
+ end
561
650
  [tag_name, attributes, attributes_hashes, object_ref, nuke_outer_whitespace,
562
- nuke_inner_whitespace, action, value, last_line || @index]
651
+ nuke_inner_whitespace, action, value, last_line || @line.index + 1]
563
652
  end
564
653
 
565
- def parse_old_attributes(line)
566
- line = line.dup
567
- last_line = @index
654
+ # @return [String] attributes_hash - Hash literal starting with `{` and ending with `}`
655
+ # @return [String] rest
656
+ # @return [Integer] last_line
657
+ def parse_old_attributes(text)
658
+ last_line = @line.index + 1
568
659
 
569
660
  begin
570
- attributes_hash, rest = balance(line, ?{, ?})
661
+ # Old attributes often look like a valid Hash literal, but it sometimes allow code like
662
+ # `{ hash, foo: bar }`, which is compiled to `_hamlout.attributes({}, nil, hash, foo: bar)`.
663
+ #
664
+ # To scan such code correctly, this scans `a( hash, foo: bar }` instead, stops when there is
665
+ # 1 more :on_embexpr_end (the last '}') than :on_embexpr_beg, and resurrects '{' afterwards.
666
+ balanced, rest = balance_tokens(text.sub(?{, METHOD_CALL_PREFIX), :on_embexpr_beg, :on_embexpr_end, count: 1)
667
+ attributes_hash = balanced.sub(METHOD_CALL_PREFIX, ?{)
571
668
  rescue SyntaxError => e
572
- if line.strip[-1] == ?, && e.message == Error.message(:unbalanced_brackets)
573
- line << "\n" << @next_line.text
669
+ if e.message == Error.message(:unbalanced_brackets) && !@template.empty?
670
+ text << "\n#{@next_line.text}"
574
671
  last_line += 1
575
672
  next_line
576
673
  retry
@@ -579,14 +676,15 @@ module Haml
579
676
  raise e
580
677
  end
581
678
 
582
- attributes_hash = attributes_hash[1...-1] if attributes_hash
583
679
  return attributes_hash, rest, last_line
584
680
  end
585
681
 
586
- def parse_new_attributes(line)
587
- line = line.dup
588
- scanner = StringScanner.new(line)
589
- last_line = @index
682
+ # @return [Array<Hash,String,nil>] - [static_attributes (Hash), dynamic_attributes (nil or String starting with `{` and ending with `}`)]
683
+ # @return [String] rest
684
+ # @return [Integer] last_line
685
+ def parse_new_attributes(text)
686
+ scanner = StringScanner.new(text)
687
+ last_line = @line.index + 1
590
688
  attributes = {}
591
689
 
592
690
  scanner.scan(/\(\s*/)
@@ -595,14 +693,15 @@ module Haml
595
693
  break if name.nil?
596
694
 
597
695
  if name == false
598
- text = (Haml::Util.balance(line, ?(, ?)) || [line]).first
696
+ scanned = Haml::Util.balance(text, ?(, ?))
697
+ text = scanned ? scanned.first : text
599
698
  raise Haml::SyntaxError.new(Error.message(:invalid_attribute_list, text.inspect), last_line - 1)
600
699
  end
601
700
  attributes[name] = value
602
701
  scanner.scan(/\s*/)
603
702
 
604
703
  if scanner.eos?
605
- line << " " << @next_line.text
704
+ text << " #{@next_line.text}"
606
705
  last_line += 1
607
706
  next_line
608
707
  scanner.scan(/\s*/)
@@ -610,12 +709,12 @@ module Haml
610
709
  end
611
710
 
612
711
  static_attributes = {}
613
- dynamic_attributes = "{"
712
+ dynamic_attributes = "{".dup
614
713
  attributes.each do |name, (type, val)|
615
714
  if type == :static
616
715
  static_attributes[name] = val
617
716
  else
618
- dynamic_attributes << inspect_obj(name) << " => " << val << ","
717
+ dynamic_attributes << "#{inspect_obj(name)} => #{val},"
619
718
  end
620
719
  end
621
720
  dynamic_attributes << "}"
@@ -625,7 +724,7 @@ module Haml
625
724
  end
626
725
 
627
726
  def parse_new_attribute(scanner)
628
- unless name = scanner.scan(/[-:\w]+/)
727
+ unless (name = scanner.scan(/[-:\w]+/))
629
728
  return if scanner.scan(/\)/)
630
729
  return false
631
730
  end
@@ -634,8 +733,8 @@ module Haml
634
733
  return name, [:static, true] unless scanner.scan(/=/) #/end
635
734
 
636
735
  scanner.scan(/\s*/)
637
- unless quote = scanner.scan(/["']/)
638
- return false unless var = scanner.scan(/(@@?|\$)?\w+/)
736
+ unless (quote = scanner.scan(/["']/))
737
+ return false unless (var = scanner.scan(/(@@?|\$)?\w+/))
639
738
  return name, [:dynamic, var]
640
739
  end
641
740
 
@@ -650,35 +749,16 @@ module Haml
650
749
 
651
750
  return name, [:static, content.first[1]] if content.size == 1
652
751
  return name, [:dynamic,
653
- '"' + content.map {|(t, v)| t == :str ? inspect_obj(v)[1...-1] : "\#{#{v}}"}.join + '"']
654
- end
655
-
656
- def raw_next_line
657
- text = @template.shift
658
- return unless text
659
-
660
- index = @template_index
661
- @template_index += 1
662
-
663
- return text, index
752
+ %!"#{content.each_with_object(''.dup) {|(t, v), s| s << (t == :str ? inspect_obj(v)[1...-1] : "\#{#{v}}")}}"!]
664
753
  end
665
754
 
666
755
  def next_line
667
- text, index = raw_next_line
668
- return unless text
669
-
670
- # :eod is a special end-of-document marker
671
- line =
672
- if text == :eod
673
- Line.new '-#', '-#', '-#', index, self, true
674
- else
675
- Line.new text.strip, text.lstrip.chomp, text, index, self, false
676
- end
756
+ line = @template.shift || raise(StopIteration)
677
757
 
678
758
  # `flat?' here is a little outdated,
679
759
  # so we have to manually check if either the previous or current line
680
760
  # closes the flat block, as well as whether a new block is opened.
681
- line_defined = instance_variable_defined?('@line')
761
+ line_defined = instance_variable_defined?(:@line)
682
762
  @line.tabs if line_defined
683
763
  unless (flat? && !closes_flat?(line) && !closes_flat?(@line)) ||
684
764
  (line_defined && @line.text[0] == ?: && line.full =~ %r[^#{@line.full[/^\s+/]}\s])
@@ -694,21 +774,17 @@ module Haml
694
774
  line && !line.text.empty? && line.full !~ /^#{@flat_spaces}/
695
775
  end
696
776
 
697
- def un_next_line(line)
698
- @template.unshift line
699
- @template_index -= 1
700
- end
701
-
702
777
  def handle_multiline(line)
703
778
  return unless is_multiline?(line.text)
704
779
  line.text.slice!(-1)
705
- while new_line = raw_next_line.first
706
- break if new_line == :eod
707
- next if new_line.strip.empty?
708
- break unless is_multiline?(new_line.strip)
709
- line.text << new_line.strip[0...-1]
780
+ loop do
781
+ new_line = @template.first
782
+ break if new_line.eod?
783
+ next @template.shift if new_line.text.strip.empty?
784
+ break unless is_multiline?(new_line.text.strip)
785
+ line.text << new_line.text.strip[0...-1]
786
+ @template.shift
710
787
  end
711
- un_next_line new_line
712
788
  end
713
789
 
714
790
  # Checks whether or not `line` is in a multiline sequence.
@@ -716,18 +792,18 @@ module Haml
716
792
  text && text.length > 1 && text[-1] == MULTILINE_CHAR_VALUE && text[-2] == ?\s && text !~ BLOCK_WITH_SPACES
717
793
  end
718
794
 
719
- def handle_ruby_multiline(text)
720
- text = text.rstrip
721
- return text unless is_ruby_multiline?(text)
722
- un_next_line @next_line.full
795
+ def handle_ruby_multiline(line)
796
+ line.text.rstrip!
797
+ return line unless is_ruby_multiline?(line.text)
723
798
  begin
724
- new_line = raw_next_line.first
725
- break if new_line == :eod
726
- next if new_line.strip.empty?
727
- text << " " << new_line.strip
728
- end while is_ruby_multiline?(new_line.strip)
799
+ # Use already fetched @next_line in the first loop. Otherwise, fetch next
800
+ new_line = new_line.nil? ? @next_line : @template.shift
801
+ break if new_line.eod?
802
+ next if new_line.text.empty?
803
+ line.text << " #{new_line.text.rstrip}"
804
+ end while is_ruby_multiline?(new_line.text)
729
805
  next_line
730
- text
806
+ line
731
807
  end
732
808
 
733
809
  # `text' is a Ruby multiline block if it:
@@ -735,15 +811,31 @@ module Haml
735
811
  # - but not "?," which is a character literal
736
812
  # (however, "x?," is a method call and not a literal)
737
813
  # - and not "?\," which is a character literal
738
- #
739
814
  def is_ruby_multiline?(text)
740
815
  text && text.length > 1 && text[-1] == ?, &&
741
- !((text[-3..-2] =~ /\W\?/) || text[-3..-2] == "?\\")
816
+ !((text[-3, 2] =~ /\W\?/) || text[-3, 2] == "?\\")
742
817
  end
743
818
 
744
819
  def balance(*args)
745
- res = Haml::Util.balance(*args)
746
- return res if res
820
+ Haml::Util.balance(*args) or raise(SyntaxError.new(Error.message(:unbalanced_brackets)))
821
+ end
822
+
823
+ # Unlike #balance, this balances Ripper tokens to balance something like `{ a: "}" }` correctly.
824
+ def balance_tokens(buf, start, finish, count: 0)
825
+ text = ''.dup
826
+ Ripper.lex(buf).each do |_, token, str|
827
+ text << str
828
+ case token
829
+ when start
830
+ count += 1
831
+ when finish
832
+ count -= 1
833
+ end
834
+
835
+ if count == 0
836
+ return text, buf.sub(text, '')
837
+ end
838
+ end
747
839
  raise SyntaxError.new(Error.message(:unbalanced_brackets))
748
840
  end
749
841
 
@@ -754,7 +846,7 @@ module Haml
754
846
  # Same semantics as block_opened?, except that block_opened? uses Line#tabs,
755
847
  # which doesn't interact well with filter lines
756
848
  def filter_opened?
757
- @next_line.full =~ (@indentation ? /^#{@indentation * @template_tabs}/ : /^\s/)
849
+ @next_line.full =~ (@indentation ? /^#{@indentation * (@template_tabs + 1)}/ : /^\s/)
758
850
  end
759
851
 
760
852
  def flat?