haml 5.1.1 → 5.2.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -16,7 +16,7 @@ module Haml
16
16
  TYPE = 1
17
17
  TEXT = 2
18
18
 
19
- IGNORED_TYPES = %i[on_sp on_ignored_nl]
19
+ IGNORED_TYPES = %i[on_sp on_ignored_nl].freeze
20
20
 
21
21
  class << self
22
22
  # @return [Boolean] - return true if AttributeParser.parse can be used.
data/lib/haml/buffer.rb CHANGED
@@ -130,18 +130,6 @@ module Haml
130
130
  @real_tabs += tab_change
131
131
  end
132
132
 
133
- def attributes(class_id, obj_ref, *attributes_hashes)
134
- attributes = class_id
135
- attributes_hashes.each do |old|
136
- result = {}
137
- old.each { |k, v| result[k.to_s] = v }
138
- AttributeBuilder.merge_attributes!(attributes, result)
139
- end
140
- AttributeBuilder.merge_attributes!(attributes, parse_object_ref(obj_ref)) if obj_ref
141
- AttributeBuilder.build_attributes(
142
- html?, @options[:attr_wrapper], @options[:escape_attrs], @options[:hyphenate_data_attrs], attributes)
143
- end
144
-
145
133
  # Remove the whitespace from the right side of the buffer string.
146
134
  # Doesn't do anything if we're at the beginning of a capture_haml block.
147
135
  def rstrip!
@@ -190,49 +178,5 @@ module Haml
190
178
  tabs = [count + @tabulation, 0].max
191
179
  @@tab_cache[tabs] ||= ' ' * tabs
192
180
  end
193
-
194
- # Takes an array of objects and uses the class and id of the first
195
- # one to create an attributes hash.
196
- # The second object, if present, is used as a prefix,
197
- # just like you can do with `dom_id()` and `dom_class()` in Rails
198
- def parse_object_ref(ref)
199
- prefix = ref[1]
200
- ref = ref[0]
201
- # Let's make sure the value isn't nil. If it is, return the default Hash.
202
- return {} if ref.nil?
203
- class_name =
204
- if ref.respond_to?(:haml_object_ref)
205
- ref.haml_object_ref
206
- else
207
- underscore(ref.class)
208
- end
209
- ref_id =
210
- if ref.respond_to?(:to_key)
211
- key = ref.to_key
212
- key.join('_') unless key.nil?
213
- else
214
- ref.id
215
- end
216
- id = "#{class_name}_#{ref_id || 'new'}"
217
- if prefix
218
- class_name = "#{ prefix }_#{ class_name}"
219
- id = "#{ prefix }_#{ id }"
220
- end
221
-
222
- { 'id'.freeze => id, 'class'.freeze => class_name }
223
- end
224
-
225
- # Changes a word from camel case to underscores.
226
- # Based on the method of the same name in Rails' Inflector,
227
- # but copied here so it'll run properly without Rails.
228
- def underscore(camel_cased_word)
229
- word = camel_cased_word.to_s.dup
230
- word.gsub!(/::/, '_')
231
- word.gsub!(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
232
- word.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
233
- word.tr!('-', '_')
234
- word.downcase!
235
- word
236
- end
237
181
  end
238
182
  end
data/lib/haml/compiler.rb CHANGED
@@ -188,30 +188,28 @@ module Haml
188
188
 
189
189
  if @options.html5?
190
190
  '<!DOCTYPE html>'
191
- else
192
- if @options.xhtml?
193
- if @node.value[:version] == "1.1"
194
- '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">'
195
- elsif @node.value[:version] == "5"
196
- '<!DOCTYPE html>'
197
- else
198
- case @node.value[:type]
199
- when "strict"; '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">'
200
- when "frameset"; '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">'
201
- when "mobile"; '<!DOCTYPE html PUBLIC "-//WAPFORUM//DTD XHTML Mobile 1.2//EN" "http://www.openmobilealliance.org/tech/DTD/xhtml-mobile12.dtd">'
202
- when "rdfa"; '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML+RDFa 1.0//EN" "http://www.w3.org/MarkUp/DTD/xhtml-rdfa-1.dtd">'
203
- when "basic"; '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML Basic 1.1//EN" "http://www.w3.org/TR/xhtml-basic/xhtml-basic11.dtd">'
204
- else '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">'
205
- end
206
- end
207
-
208
- elsif @options.html4?
191
+ elsif @options.xhtml?
192
+ if @node.value[:version] == "1.1"
193
+ '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">'
194
+ elsif @node.value[:version] == "5"
195
+ '<!DOCTYPE html>'
196
+ else
209
197
  case @node.value[:type]
210
- when "strict"; '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">'
211
- when "frameset"; '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Frameset//EN" "http://www.w3.org/TR/html4/frameset.dtd">'
212
- else '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">'
198
+ when "strict"; '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">'
199
+ when "frameset"; '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">'
200
+ when "mobile"; '<!DOCTYPE html PUBLIC "-//WAPFORUM//DTD XHTML Mobile 1.2//EN" "http://www.openmobilealliance.org/tech/DTD/xhtml-mobile12.dtd">'
201
+ when "rdfa"; '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML+RDFa 1.0//EN" "http://www.w3.org/MarkUp/DTD/xhtml-rdfa-1.dtd">'
202
+ when "basic"; '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML Basic 1.1//EN" "http://www.w3.org/TR/xhtml-basic/xhtml-basic11.dtd">'
203
+ else '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">'
213
204
  end
214
205
  end
206
+
207
+ elsif @options.html4?
208
+ case @node.value[:type]
209
+ when "strict"; '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">'
210
+ when "frameset"; '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Frameset//EN" "http://www.w3.org/TR/html4/frameset.dtd">'
211
+ else '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">'
212
+ end
215
213
  end
216
214
  end
217
215
 
data/lib/haml/error.rb CHANGED
@@ -5,29 +5,29 @@ module Haml
5
5
  class Error < StandardError
6
6
 
7
7
  MESSAGES = {
8
- :bad_script_indent => '"%s" is indented at wrong level: expected %d, but was at %d.',
9
- :cant_run_filter => 'Can\'t run "%s" filter; you must require its dependencies first',
10
- :cant_use_tabs_and_spaces => "Indentation can't use both tabs and spaces.",
11
- :deeper_indenting => "The line was indented %d levels deeper than the previous line.",
12
- :filter_not_defined => 'Filter "%s" is not defined.',
13
- :gem_install_filter_deps => '"%s" filter\'s %s dependency missing: try installing it or adding it to your Gemfile',
14
- :illegal_element => "Illegal element: classes and ids must have values.",
15
- :illegal_nesting_content => "Illegal nesting: nesting within a tag that already has content is illegal.",
16
- :illegal_nesting_header => "Illegal nesting: nesting within a header command is illegal.",
17
- :illegal_nesting_line => "Illegal nesting: content can't be both given on the same line as %%%s and nested within it.",
18
- :illegal_nesting_plain => "Illegal nesting: nesting within plain text is illegal.",
19
- :illegal_nesting_self_closing => "Illegal nesting: nesting within a self-closing tag is illegal.",
20
- :inconsistent_indentation => "Inconsistent indentation: %s used for indentation, but the rest of the document was indented using %s.",
21
- :indenting_at_start => "Indenting at the beginning of the document is illegal.",
22
- :install_haml_contrib => 'To use the "%s" filter, please install the haml-contrib gem.',
23
- :invalid_attribute_list => 'Invalid attribute list: %s.',
24
- :invalid_filter_name => 'Invalid filter name ":%s".',
25
- :invalid_tag => 'Invalid tag: "%s".',
26
- :missing_if => 'Got "%s" with no preceding "if"',
27
- :no_ruby_code => "There's no Ruby code for %s to evaluate.",
28
- :self_closing_content => "Self-closing tags can't have content.",
29
- :unbalanced_brackets => 'Unbalanced brackets.',
30
- :no_end => <<-END
8
+ bad_script_indent: '"%s" is indented at wrong level: expected %d, but was at %d.',
9
+ cant_run_filter: 'Can\'t run "%s" filter; you must require its dependencies first',
10
+ cant_use_tabs_and_spaces: "Indentation can't use both tabs and spaces.",
11
+ deeper_indenting: "The line was indented %d levels deeper than the previous line.",
12
+ filter_not_defined: 'Filter "%s" is not defined.',
13
+ gem_install_filter_deps: '"%s" filter\'s %s dependency missing: try installing it or adding it to your Gemfile',
14
+ illegal_element: "Illegal element: classes and ids must have values.",
15
+ illegal_nesting_content: "Illegal nesting: nesting within a tag that already has content is illegal.",
16
+ illegal_nesting_header: "Illegal nesting: nesting within a header command is illegal.",
17
+ illegal_nesting_line: "Illegal nesting: content can't be both given on the same line as %%%s and nested within it.",
18
+ illegal_nesting_plain: "Illegal nesting: nesting within plain text is illegal.",
19
+ illegal_nesting_self_closing: "Illegal nesting: nesting within a self-closing tag is illegal.",
20
+ inconsistent_indentation: "Inconsistent indentation: %s used for indentation, but the rest of the document was indented using %s.",
21
+ indenting_at_start: "Indenting at the beginning of the document is illegal.",
22
+ install_haml_contrib: 'To use the "%s" filter, please install the haml-contrib gem.',
23
+ invalid_attribute_list: 'Invalid attribute list: %s.',
24
+ invalid_filter_name: 'Invalid filter name ":%s".',
25
+ invalid_tag: 'Invalid tag: "%s".',
26
+ missing_if: 'Got "%s" with no preceding "if"',
27
+ no_ruby_code: "There's no Ruby code for %s to evaluate.",
28
+ self_closing_content: "Self-closing tags can't have content.",
29
+ unbalanced_brackets: 'Unbalanced brackets.',
30
+ no_end: <<-END
31
31
  You don't need to use "- end" in Haml. Un-indent to close a block:
32
32
  - if foo?
33
33
  %strong Foo!
@@ -35,7 +35,7 @@ You don't need to use "- end" in Haml. Un-indent to close a block:
35
35
  Not foo.
36
36
  %p This line is un-indented, so it isn't part of the "if" block
37
37
  END
38
- }
38
+ }.freeze
39
39
 
40
40
  def self.message(key, *args)
41
41
  string = MESSAGES[key] or raise "[HAML BUG] No error messages for #{key}"
@@ -4,30 +4,31 @@ module Haml
4
4
  # Like Temple::Filters::Escapable, but with support for escaping by
5
5
  # Haml::Herlpers.html_escape and Haml::Herlpers.escape_once.
6
6
  class Escapable < Temple::Filter
7
+ # Special value of `flag` to ignore html_safe?
8
+ EscapeSafeBuffer = Struct.new(:value)
9
+
7
10
  def initialize(*)
8
11
  super
9
- @escape_code = "::Haml::Helpers.html_escape((%s))"
10
- @escaper = eval("proc {|v| #{@escape_code % 'v'} }")
11
- @once_escape_code = "::Haml::Helpers.escape_once((%s))"
12
- @once_escaper = eval("proc {|v| #{@once_escape_code % 'v'} }")
13
12
  @escape = false
13
+ @escape_safe_buffer = false
14
14
  end
15
15
 
16
16
  def on_escape(flag, exp)
17
- old = @escape
18
- @escape = flag
17
+ old_escape, old_escape_safe_buffer = @escape, @escape_safe_buffer
18
+ @escape_safe_buffer = flag.is_a?(EscapeSafeBuffer)
19
+ @escape = @escape_safe_buffer ? flag.value : flag
19
20
  compile(exp)
20
21
  ensure
21
- @escape = old
22
+ @escape, @escape_safe_buffer = old_escape, old_escape_safe_buffer
22
23
  end
23
24
 
24
25
  # The same as Haml::AttributeBuilder.build_attributes
25
26
  def on_static(value)
26
27
  [:static,
27
28
  if @escape == :once
28
- @once_escaper[value]
29
+ escape_once(value)
29
30
  elsif @escape
30
- @escaper[value]
31
+ escape(value)
31
32
  else
32
33
  value
33
34
  end
@@ -38,13 +39,39 @@ module Haml
38
39
  def on_dynamic(value)
39
40
  [:dynamic,
40
41
  if @escape == :once
41
- @once_escape_code % value
42
+ escape_once_code(value)
42
43
  elsif @escape
43
- @escape_code % value
44
+ escape_code(value)
44
45
  else
45
46
  "(#{value}).to_s"
46
47
  end
47
48
  ]
48
49
  end
50
+
51
+ private
52
+
53
+ def escape_once(value)
54
+ if @escape_safe_buffer
55
+ ::Haml::Helpers.escape_once_without_haml_xss(value)
56
+ else
57
+ ::Haml::Helpers.escape_once(value)
58
+ end
59
+ end
60
+
61
+ def escape(value)
62
+ if @escape_safe_buffer
63
+ ::Haml::Helpers.html_escape_without_haml_xss(value)
64
+ else
65
+ ::Haml::Helpers.html_escape(value)
66
+ end
67
+ end
68
+
69
+ def escape_once_code(value)
70
+ "::Haml::Helpers.escape_once#{('_without_haml_xss' if @escape_safe_buffer)}((#{value}))"
71
+ end
72
+
73
+ def escape_code(value)
74
+ "::Haml::Helpers.html_escape#{('_without_haml_xss' if @escape_safe_buffer)}((#{value}))"
75
+ end
49
76
  end
50
77
  end
data/lib/haml/exec.rb CHANGED
@@ -121,7 +121,7 @@ module Haml
121
121
  @options[:input], @options[:output] = input, output
122
122
  end
123
123
 
124
- COLORS = { :red => 31, :green => 32, :yellow => 33 }
124
+ COLORS = {red: 31, green: 32, yellow: 33}.freeze
125
125
 
126
126
  # Prints a status message about performing the given action,
127
127
  # colored using the given color (via terminal escapes) if possible.
data/lib/haml/helpers.rb CHANGED
@@ -593,7 +593,7 @@ MESSAGE
593
593
  end
594
594
 
595
595
  # Characters that need to be escaped to HTML entities from user input
596
- HTML_ESCAPE = { '&' => '&amp;', '<' => '&lt;', '>' => '&gt;', '"' => '&quot;', "'" => '&#39;' }
596
+ HTML_ESCAPE = {'&' => '&amp;', '<' => '&lt;', '>' => '&gt;', '"' => '&quot;', "'" => '&#39;'}.freeze
597
597
 
598
598
  HTML_ESCAPE_REGEX = /['"><&]/
599
599
 
@@ -607,9 +607,12 @@ MESSAGE
607
607
  # @param text [String] The string to sanitize
608
608
  # @return [String] The sanitized string
609
609
  def html_escape(text)
610
- ERB::Util.html_escape(text)
610
+ CGI.escapeHTML(text.to_s)
611
611
  end
612
612
 
613
+ # Always escape text regardless of html_safe?
614
+ alias_method :html_escape_without_haml_xss, :html_escape
615
+
613
616
  HTML_ESCAPE_ONCE_REGEX = /['"><]|&(?!(?:[a-zA-Z]+|#(?:\d+|[xX][0-9a-fA-F]+));)/
614
617
 
615
618
  # Escapes HTML entities in `text`, but without escaping an ampersand
@@ -622,6 +625,9 @@ MESSAGE
622
625
  text.gsub(HTML_ESCAPE_ONCE_REGEX, HTML_ESCAPE)
623
626
  end
624
627
 
628
+ # Always escape text once regardless of html_safe?
629
+ alias_method :escape_once_without_haml_xss, :escape_once
630
+
625
631
  # Returns whether or not the current template is a Haml template.
626
632
  #
627
633
  # This function, unlike other {Haml::Helpers} functions,
@@ -8,12 +8,15 @@ module Haml
8
8
  # to work with Rails' XSS protection methods.
9
9
  module XssMods
10
10
  def self.included(base)
11
- %w[html_escape find_and_preserve preserve list_of surround
12
- precede succeed capture_haml haml_concat haml_internal_concat haml_indent
13
- escape_once].each do |name|
11
+ %w[find_and_preserve preserve list_of surround
12
+ precede succeed capture_haml haml_concat haml_internal_concat haml_indent].each do |name|
14
13
  base.send(:alias_method, "#{name}_without_haml_xss", name)
15
14
  base.send(:alias_method, name, "#{name}_with_haml_xss")
16
15
  end
16
+ # Those two always have _without_haml_xss
17
+ %w[html_escape escape_once].each do |name|
18
+ base.send(:alias_method, name, "#{name}_with_haml_xss")
19
+ end
17
20
  end
18
21
 
19
22
  # Don't escape text that's already safe,
data/lib/haml/options.rb CHANGED
@@ -5,44 +5,40 @@ module Haml
5
5
  # understands. Please see the {file:REFERENCE.md#options Haml Reference} to
6
6
  # learn how to set the options.
7
7
  class Options
8
-
9
8
  @valid_formats = [:html4, :html5, :xhtml]
10
-
11
9
  @buffer_option_keys = [:autoclose, :preserve, :attr_wrapper, :format,
12
10
  :encoding, :escape_html, :escape_filter_interpolations, :escape_attrs, :hyphenate_data_attrs, :cdata]
13
11
 
14
- # The default option values.
15
- # @return Hash
16
- def self.defaults
17
- @defaults ||= Haml::TempleEngine.options.to_hash.merge(encoding: 'UTF-8')
18
- end
12
+ class << self
13
+ # The default option values.
14
+ # @return Hash
15
+ def defaults
16
+ @defaults ||= Haml::TempleEngine.options.to_hash.merge(encoding: 'UTF-8')
17
+ end
19
18
 
20
- # An array of valid values for the `:format` option.
21
- # @return Array
22
- def self.valid_formats
23
- @valid_formats
24
- end
19
+ # An array of valid values for the `:format` option.
20
+ # @return Array
21
+ attr_reader :valid_formats
25
22
 
26
- # An array of keys that will be used to provide a hash of options to
27
- # {Haml::Buffer}.
28
- # @return Hash
29
- def self.buffer_option_keys
30
- @buffer_option_keys
31
- end
23
+ # An array of keys that will be used to provide a hash of options to
24
+ # {Haml::Buffer}.
25
+ # @return Hash
26
+ attr_reader :buffer_option_keys
32
27
 
33
- # Returns a subset of defaults: those that {Haml::Buffer} cares about.
34
- # @return [{Symbol => Object}] The options hash
35
- def self.buffer_defaults
36
- @buffer_defaults ||= buffer_option_keys.inject({}) do |hash, key|
37
- hash.merge(key => defaults[key])
28
+ # Returns a subset of defaults: those that {Haml::Buffer} cares about.
29
+ # @return [{Symbol => Object}] The options hash
30
+ def buffer_defaults
31
+ @buffer_defaults ||= buffer_option_keys.inject({}) do |hash, key|
32
+ hash.merge(key => defaults[key])
33
+ end
38
34
  end
39
- end
40
35
 
41
- def self.wrap(options)
42
- if options.is_a?(Options)
43
- options
44
- else
45
- Options.new(options)
36
+ def wrap(options)
37
+ if options.is_a?(Options)
38
+ options
39
+ else
40
+ Options.new(options)
41
+ end
46
42
  end
47
43
  end
48
44
 
@@ -139,7 +135,7 @@ module Haml
139
135
  # formatting errors.
140
136
  #
141
137
  # Defaults to `false`.
142
- attr_reader :remove_whitespace
138
+ attr_accessor :remove_whitespace
143
139
 
144
140
  # Whether or not attribute hashes and Ruby scripts designated by `=` or `~`
145
141
  # should be evaluated. If this is `true`, said scripts are rendered as empty
@@ -175,7 +171,7 @@ module Haml
175
171
  # Key is filter name in String and value is Class to use. Defaults to {}.
176
172
  attr_accessor :filters
177
173
 
178
- def initialize(values = {}, &block)
174
+ def initialize(values = {})
179
175
  defaults.each {|k, v| instance_variable_set :"@#{k}", v}
180
176
  values.each {|k, v| send("#{k}=", v) if defaults.has_key?(k) && !v.nil?}
181
177
  yield if block_given?
@@ -245,10 +241,6 @@ module Haml
245
241
  xhtml? || @cdata
246
242
  end
247
243
 
248
- def remove_whitespace=(value)
249
- @remove_whitespace = value
250
- end
251
-
252
244
  def encoding=(value)
253
245
  return unless value
254
246
  @encoding = value.is_a?(Encoding) ? value.name : value.to_s
data/lib/haml/parser.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'ripper'
3
4
  require 'strscan'
4
5
 
5
6
  module Haml
@@ -61,7 +62,7 @@ module Haml
61
62
  SILENT_SCRIPT,
62
63
  ESCAPE,
63
64
  FILTER
64
- ]
65
+ ].freeze
65
66
 
66
67
  # The value of the character that designates that a line is part
67
68
  # of a multiline string.
@@ -75,8 +76,8 @@ module Haml
75
76
  #
76
77
  BLOCK_WITH_SPACES = /do\s*\|\s*[^\|]*\s+\|\z/
77
78
 
78
- MID_BLOCK_KEYWORDS = %w[else elsif rescue ensure end when]
79
- 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
80
81
  # Try to parse assignments to block starters as best as possible
81
82
  START_BLOCK_KEYWORD_REGEX = /(?:\w+(?:,\s*\w+)*\s*=\s*)?(#{START_BLOCK_KEYWORDS.join('|')})/
82
83
  BLOCK_KEYWORD_REGEX = /^-?\s*(?:(#{MID_BLOCK_KEYWORDS.join('|')})|#{START_BLOCK_KEYWORD_REGEX.source})\b/
@@ -90,6 +91,9 @@ module Haml
90
91
  ID_KEY = 'id'.freeze
91
92
  CLASS_KEY = 'class'.freeze
92
93
 
94
+ # Used for scanning old attributes, substituting the first '{'
95
+ METHOD_CALL_PREFIX = 'a('
96
+
93
97
  def initialize(options)
94
98
  @options = Options.wrap(options)
95
99
  # Record the indent levels of "if" statements to validate the subsequent
@@ -202,7 +206,7 @@ module Haml
202
206
  end
203
207
 
204
208
  def inspect
205
- %Q[(#{type} #{value.inspect}#{children.each_with_object('') {|c, s| s << "\n#{c.inspect.gsub!(/^/, ' ')}"}})]
209
+ %Q[(#{type} #{value.inspect}#{children.each_with_object(''.dup) {|c, s| s << "\n#{c.inspect.gsub!(/^/, ' ')}"}})].dup
206
210
  end
207
211
  end
208
212
 
@@ -307,7 +311,7 @@ module Haml
307
311
  return ParseNode.new(:plain, line.index + 1, :text => line.text)
308
312
  end
309
313
 
310
- escape_html = @options.escape_html if escape_html.nil?
314
+ escape_html = @options.escape_html && @options.mime_type != 'text/plain' if escape_html.nil?
311
315
  line.text = unescape_interpolation(line.text, escape_html)
312
316
  script(line, false)
313
317
  end
@@ -651,13 +655,18 @@ module Haml
651
655
  # @return [String] rest
652
656
  # @return [Integer] last_line
653
657
  def parse_old_attributes(text)
654
- text = text.dup
655
658
  last_line = @line.index + 1
656
659
 
657
660
  begin
658
- attributes_hash, rest = balance(text, ?{, ?})
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, ?{)
659
668
  rescue SyntaxError => e
660
- if text.strip[-1] == ?, && e.message == Error.message(:unbalanced_brackets)
669
+ if e.message == Error.message(:unbalanced_brackets) && !@template.empty?
661
670
  text << "\n#{@next_line.text}"
662
671
  last_line += 1
663
672
  next_line
@@ -811,6 +820,25 @@ module Haml
811
820
  Haml::Util.balance(*args) or raise(SyntaxError.new(Error.message(:unbalanced_brackets)))
812
821
  end
813
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
839
+ raise SyntaxError.new(Error.message(:unbalanced_brackets))
840
+ end
841
+
814
842
  def block_opened?
815
843
  @next_line.tabs > @line.tabs
816
844
  end