haml 7.3.0 → 7.5.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 381430279e5ca21c3d03f899c686f972609e54633c8e530609511e852597b7ef
4
- data.tar.gz: a108268dccda6dc0e3ca014e484a7bcc0f50c973771a0057a086d75de2847495
3
+ metadata.gz: 6436b78efc92438b9bd1a948dd0672bc8d3798c654fa2846d310386ef0552084
4
+ data.tar.gz: b8c2657b36c209e49c0e58c95470c45cc8213e30b1eb17458e0550db37bd616e
5
5
  SHA512:
6
- metadata.gz: b5f8d7bcc23e52be3fd4ed443ec3fdf171773f9c9aaacd1705d92ec09a3ef372adf13a8715ca4b4d039aa2a07fc2fdbb7e59eef30a22f55be2823bb0c7e23f0d
7
- data.tar.gz: 954f3aa91fa3f49499d05dd1503056dd65a439519c5b2bcc300a6cf3dfbacf686717c8aa1e10ddb1ca7fa779504d92561a33e8a7d729b8013967cde102636fca
6
+ metadata.gz: 0530e52beb2fd093dfeb7d55c107b92a84f91e854bd38ed2e919349fcb820043cd3dd6e328ba4b2b24aa1b64ee8fcf2faf4a36dae1da60cb1b0a2b6abef7f531
7
+ data.tar.gz: b5a94fe5203318f1eaefa08b86e418090b8d0ef48cb94afa219512d87b116f568833cdf42e8f24a0a5e540d47b4b98dfb8a04f9e372bfe99d912d01dc41c2c24
data/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # Haml Changelog
2
2
 
3
+ ## 7.5.0
4
+
5
+ * Compile a multi-line attribute hash statically https://github.com/haml/haml/pull/1222
6
+ * Errors from multi-line attributes will be reported on the tag's line instead of the expression that raised the error.
7
+
8
+ ## 7.4.1
9
+
10
+ * Build the preserve regex once per tag list instead of per call https://github.com/haml/haml/pull/1220
11
+
12
+ ## 7.4.0
13
+
14
+ * `Haml::BOOLEAN_ATTRIBUTES` is changed from Array to Set https://github.com/haml/haml/pull/1216
15
+ * A guard on `respond_to?(:html_safe?)` is removed from `Haml::Util.escape_html_safe` https://github.com/haml/haml/pull/1217
16
+ * Keep interpolated text in the source encoding, fixing the `Encoding::CompatibilityError` remaining on an ASCII-8BIT
17
+ template source that mixes plain text and interpolation with non-ASCII characters https://github.com/haml/haml/issues/1218
18
+
19
+ ## 7.3.1
20
+
21
+ * Keep Prism-derived fragments in the source encoding, fixing `Encoding::CompatibilityError` on an ASCII-8BIT
22
+ template source with non-ASCII characters (regression in 7.3.0) https://github.com/haml/haml/issues/1218
23
+
3
24
  ## 7.3.0
4
25
 
5
26
  * Replace Ripper with Prism https://github.com/haml/haml/pull/1214
data/REFERENCE.md CHANGED
@@ -320,7 +320,7 @@ or using `true` and `false`:
320
320
  %input(selected=true)
321
321
 
322
322
  This feature works only for attributes that are included in
323
- [`Haml::BOOLEAN_ATTRIBUTES`](https://github.com/haml/haml/blob/main/lib/haml/attribute_compiler.rb#L8),
323
+ [`Haml::BOOLEAN_ATTRIBUTES`](https://github.com/haml/haml/blob/main/lib/haml/attribute_compiler.rb#L9),
324
324
  as well as `data-` and `aria-` attributes.
325
325
 
326
326
  %input{'data-hidden' => false}
@@ -19,10 +19,12 @@ module Haml::AttributeBuilder
19
19
  buf << build_data(escape_attrs, quote, format, *hash[key])
20
20
  when 'aria'
21
21
  buf << build_aria(escape_attrs, quote, format, *hash[key])
22
- when *Haml::BOOLEAN_ATTRIBUTES, /\Adata-/, /\Aaria-/
23
- build_boolean!(escape_attrs, quote, format, buf, key, hash[key])
24
22
  else
25
- buf << " #{key}=#{quote}#{escape_html(escape_attrs, hash[key].to_s)}#{quote}"
23
+ if boolean_attribute?(key)
24
+ build_boolean!(escape_attrs, quote, format, buf, key, hash[key])
25
+ else
26
+ buf << " #{key}=#{quote}#{escape_html(escape_attrs, hash[key].to_s)}#{quote}"
27
+ end
26
28
  end
27
29
  end
28
30
  buf.join
@@ -72,6 +74,10 @@ module Haml::AttributeBuilder
72
74
 
73
75
  private
74
76
 
77
+ def boolean_attribute?(key)
78
+ Haml::BOOLEAN_ATTRIBUTES.include?(key) || key.start_with?('data-', 'aria-')
79
+ end
80
+
75
81
  def build_data_attribute(key, escape_attrs, quote, format, *hashes)
76
82
  attrs = []
77
83
  if hashes.size > 1 && hashes.all? { |h| h.is_a?(Hash) }
@@ -98,14 +104,18 @@ module Haml::AttributeBuilder
98
104
  flattened = {}
99
105
 
100
106
  attributes.each do |key, value|
107
+ # Recursing into a hash which contains itself would never end.
108
+ next if value.equal?(attributes)
109
+
101
110
  case value
102
- when attributes
103
111
  when Hash
104
112
  flatten_attributes(value).each do |k, v|
105
113
  if k.nil?
106
114
  flattened[key] = v
107
115
  else
108
- flattened["#{key}-#{k.to_s.tr('_', '-')}"] = v
116
+ k = k.is_a?(Symbol) ? k.name : k.to_s
117
+ k = k.tr('_', '-') if k.include?('_')
118
+ flattened["#{key}-#{k}"] = v
109
119
  end
110
120
  end
111
121
  else
@@ -122,7 +132,8 @@ module Haml::AttributeBuilder
122
132
  raise ArgumentError, "Non-hash object is given to attributes!"
123
133
  end
124
134
  hash.each do |key, value|
125
- key = key.to_s
135
+ # Symbol#name hands back the interned String; to_s allocates one per render.
136
+ key = key.is_a?(Symbol) ? key.name : key.to_s
126
137
  case key
127
138
  when 'id', 'class', 'data', 'aria'
128
139
  merged[key] ||= []
@@ -1,16 +1,18 @@
1
1
  # frozen_string_literal: true
2
+ require 'set'
2
3
  require 'haml/attribute_builder'
3
4
  require 'haml/attribute_parser'
4
5
  require 'haml/ruby_expression'
6
+ require 'haml/temple_line_counter'
5
7
 
6
8
  module Haml
7
- # The list of boolean attributes. You may add custom attributes to this constant.
8
- BOOLEAN_ATTRIBUTES = %w[disabled readonly multiple checked autobuffer
9
+ # The set of boolean attributes. You may add custom attributes to this constant.
10
+ BOOLEAN_ATTRIBUTES = Set.new(%w[disabled readonly multiple checked autobuffer
9
11
  autoplay controls loop selected hidden scoped async
10
12
  defer reversed ismap seamless muted required
11
13
  autofocus novalidate formnovalidate open pubdate
12
14
  itemscope allowfullscreen default inert sortable
13
- truespeed typemustmatch download]
15
+ truespeed typemustmatch download])
14
16
 
15
17
  class AttributeCompiler
16
18
  def initialize(identity, options)
@@ -23,12 +25,15 @@ module Haml
23
25
  def compile(node)
24
26
  hashes = []
25
27
  return runtime_compile(node) if node.value[:object_ref] != :nil
26
- [node.value[:dynamic_attributes].new, node.value[:dynamic_attributes].old].compact.each do |attribute_str|
28
+ dynamic_attributes = node.value[:dynamic_attributes]
29
+ [dynamic_attributes.new, dynamic_attributes.old].compact.each do |attribute_str|
27
30
  hash = AttributeParser.parse(attribute_str)
28
31
  return runtime_compile(node) if hash.nil? || hash.any? { |_key, value| value.empty? }
29
32
  hashes << hash
30
33
  end
31
- static_compile(node.value[:attributes], hashes)
34
+ temple = static_compile(node.value[:attributes], hashes)
35
+ restore_newlines!(temple, dynamic_attributes)
36
+ temple
32
37
  end
33
38
 
34
39
  private
@@ -55,15 +60,24 @@ module Haml
55
60
  compile_class!(temple, key, values)
56
61
  when 'data', 'aria'
57
62
  compile_data!(temple, key, values)
58
- when *BOOLEAN_ATTRIBUTES, /\Adata-/, /\Aaria-/
59
- compile_boolean!(temple, key, values)
60
63
  else
61
- compile_common!(temple, key, values)
64
+ if BOOLEAN_ATTRIBUTES.include?(key) || key.start_with?('data-', 'aria-')
65
+ compile_boolean!(temple, key, values)
66
+ else
67
+ compile_common!(temple, key, values)
68
+ end
62
69
  end
63
70
  end
64
71
  temple
65
72
  end
66
73
 
74
+ # ChildrenCompiler counts on the tag spanning as many lines as its attribute source does.
75
+ def restore_newlines!(temple, dynamic_attributes)
76
+ missing = dynamic_attributes.newline_count
77
+ missing -= TempleLineCounter.count_lines(temple) if missing > 0
78
+ missing.times { temple << [:newline] }
79
+ end
80
+
67
81
  def compile_id!(temple, key, values)
68
82
  build_code = attribute_builder(:id, values)
69
83
  if values.all? { |type, exp| type == :static || Temple::StaticAnalyzer.static?(exp) }
@@ -15,15 +15,11 @@ module Haml
15
15
  end
16
16
 
17
17
  # @return [Hash,nil] - keys and values are the attribute source as written, or nil if
18
- # the text is not a Hash literal whose keys are all static.
18
+ # the text is not a Hash literal whose keys are all static, or if it holds a heredoc.
19
19
  def parse(text)
20
20
  exp = wrap_bracket(text)
21
- # A multi-line hash is left to the runtime, which keeps the [:newline] bookkeeping of
22
- # the compiled code correct. Compiling it statically is a separate optimization.
23
- return if exp.include?("\n")
24
-
25
21
  node = hash_node(exp)
26
- return if node.nil?
22
+ return if node.nil? || contains_heredoc?(exp, node)
27
23
 
28
24
  hash = {}
29
25
  node.elements.each do |element|
@@ -32,7 +28,7 @@ module Haml
32
28
  key = static_key(element.key)
33
29
  return if key.nil?
34
30
 
35
- hash[key] = value_source(element.value)
31
+ hash[in_source_encoding(key, exp)] = in_source_encoding(value_source(element.value), exp)
36
32
  end
37
33
  hash
38
34
  end
@@ -58,6 +54,12 @@ module Haml
58
54
  node if node.is_a?(Prism::HashNode)
59
55
  end
60
56
 
57
+ # A heredoc's body lies outside its node, so the value's slice would not be the value.
58
+ # Its opener always spells `<<`, which spares the tree walk for nearly every hash.
59
+ def contains_heredoc?(exp, node)
60
+ exp.include?('<<') && !node.breadth_first_search { |n| n.respond_to?(:heredoc?) && n.heredoc? }.nil?
61
+ end
62
+
61
63
  # The key as written between its delimiters, not unescaped: an escape has to reach the
62
64
  # attribute name as the source spelled it, like the `\0` of `{ "a\0b" => 1 }`.
63
65
  def static_key(key)
@@ -74,5 +76,15 @@ module Haml
74
76
 
75
77
  value.slice
76
78
  end
79
+
80
+ # Prism tags its slices with the encoding it parsed the source under (UTF-8 for a
81
+ # BINARY source), while the rest of the compiled template stays in the source
82
+ # encoding. Mixing the two raises Encoding::CompatibilityError once both sides hold
83
+ # non-ASCII bytes, so bring everything back to the source encoding.
84
+ def in_source_encoding(string, source)
85
+ return string if string.encoding == source.encoding
86
+
87
+ string.dup.force_encoding(source.encoding)
88
+ end
77
89
  end
78
90
  end
@@ -40,9 +40,7 @@ module Haml
40
40
  when :script, :silent_script
41
41
  @lineno += 1
42
42
  when :tag
43
- [node.value[:dynamic_attributes].new, node.value[:dynamic_attributes].old].compact.each do |attribute_hash|
44
- @lineno += attribute_hash.count("\n")
45
- end
43
+ @lineno += node.value[:dynamic_attributes].newline_count
46
44
  @lineno += 1 if node.children.empty? && node.value[:parse]
47
45
  end
48
46
 
@@ -1,18 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
  require 'temple/static_analyzer'
3
+ require 'haml/helpers'
4
+ require 'haml/preserver'
3
5
  require 'haml/ruby_expression'
4
6
  require 'haml/string_splitter'
5
7
 
6
8
  module Haml
7
9
  class Compiler
8
10
  class ScriptCompiler
9
- def self.find_and_preserve(input, tags)
10
- tags = tags.map { |tag| Regexp.escape(tag) }.join('|')
11
- re = /<(#{tags})([^>]*)>(.*?)(<\/\1>)/im
12
- input.to_s.gsub(re) do |s|
13
- s =~ re # Can't rely on $1, etc. existing since Rails' SafeBuffer#gsub is incompatible
14
- "<#{$1}#{$2}>#{Haml::Helpers.preserve($3)}</#{$1}>"
15
- end
11
+ def self.find_and_preserve(input, tags = ::Haml::Preserver::DEFAULT_TAGS)
12
+ ::Haml::Preserver.find_and_preserve(input, tags) { |content| ::Haml::Helpers.preserve(content) }
16
13
  end
17
14
 
18
15
  def initialize(identity, options)
@@ -69,7 +66,7 @@ module Haml
69
66
  if node.value[:escape_html]
70
67
  str = Haml::Util.escape_html(str)
71
68
  elsif node.value[:preserve]
72
- str = ScriptCompiler.find_and_preserve(str, %w(textarea pre code))
69
+ str = ScriptCompiler.find_and_preserve(str)
73
70
  end
74
71
  [:multi, [:static, str], [:newline]]
75
72
  end
@@ -104,7 +101,7 @@ module Haml
104
101
  end
105
102
 
106
103
  def find_and_preserve(code)
107
- %Q[::Haml::Compiler::ScriptCompiler.find_and_preserve(#{code}, %w(textarea pre code))]
104
+ %Q[::Haml::Compiler::ScriptCompiler.find_and_preserve(#{code})]
108
105
  end
109
106
 
110
107
  def escape_html(temple)
data/lib/haml/helpers.rb CHANGED
@@ -3,7 +3,7 @@ module Haml
3
3
  module Helpers
4
4
  def self.preserve(input)
5
5
  s = input.to_s.chomp("\n")
6
- s.gsub!(/\n/, '&#x000A;')
6
+ s.gsub!("\n", '&#x000A;')
7
7
  s.delete!("\r")
8
8
  s
9
9
  end
data/lib/haml/parser.rb CHANGED
@@ -247,6 +247,12 @@ module Haml
247
247
  [new, stripped_old].compact.join(', ')
248
248
  end
249
249
 
250
+ # The lines a tag spans beyond its first because of these attributes, which is what
251
+ # the compiled tag has to span too for the line numbers after it to hold.
252
+ def newline_count
253
+ (new ? new.count("\n") : 0) + (old ? old.count("\n") : 0)
254
+ end
255
+
250
256
  private
251
257
 
252
258
  # For `%foo{ { foo: 1 }, bar: 2 }`, :old is "{ { foo: 1 }, bar: 2 }" and this method returns " { foo: 1 }, bar: 2 " for last argument.
@@ -444,7 +450,8 @@ module Haml
444
450
  end
445
451
  else
446
452
  if Util.contains_interpolation?(value)
447
- value = Util.unescape_interpolation(value, escape_html)
453
+ value = Util.unescape_interpolation(value)
454
+ escape_interpolation = true if escape_html
448
455
  parse = true
449
456
  escape_html = false
450
457
  end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+ module Haml
3
+ # find_and_preserve, shared by compiled templates and RailsHelpers.
4
+ #
5
+ # @api private
6
+ module Preserver
7
+ DEFAULT_TAGS = %w[textarea pre code].freeze
8
+
9
+ def self.build_regex(tags)
10
+ # An empty entry would add an alternative matching `<>`.
11
+ pattern = tags.reject(&:empty?).map { |tag| Regexp.escape(tag) }.join('|')
12
+ /<(#{pattern})([^>]*)>(.*?)(<\/\1>)/im
13
+ end
14
+ private_class_method :build_regex
15
+
16
+ DEFAULT_REGEX = build_regex(DEFAULT_TAGS)
17
+ private_constant :DEFAULT_REGEX
18
+
19
+ # Past this many lists the merge below turns quadratic, so the cache starts over.
20
+ MAX_REGEXES = 64
21
+ private_constant :MAX_REGEXES
22
+
23
+ INITIAL_REGEXES = { DEFAULT_TAGS => DEFAULT_REGEX }.freeze
24
+ private_constant :INITIAL_REGEXES
25
+
26
+ @regexes = INITIAL_REGEXES
27
+
28
+ # The cache is replaced, never mutated, so a racing writer at worst rebuilds a regex.
29
+ def self.regex(tags)
30
+ return DEFAULT_REGEX if tags.equal?(DEFAULT_TAGS)
31
+
32
+ cache = @regexes
33
+ cache[tags] || begin
34
+ regex = build_regex(tags)
35
+ # Copied so a caller mutating its own list, or a String in it, cannot strand the entry.
36
+ key = tags.map { |tag| tag.dup.freeze }.freeze
37
+ cache = INITIAL_REGEXES if cache.size >= MAX_REGEXES
38
+ @regexes = cache.merge(key => regex).freeze
39
+ regex
40
+ end
41
+ end
42
+
43
+ def self.find_and_preserve(input, tags)
44
+ re = regex(tags)
45
+ input.to_s.gsub(re) do |s|
46
+ s =~ re # Can't rely on $1, etc. existing since Rails' SafeBuffer#gsub is incompatible
47
+ name, attributes, content = $1, $2, $3
48
+ "<#{name}#{attributes}>#{yield(content)}</#{name}>"
49
+ end
50
+ end
51
+ end
52
+ end
@@ -1,5 +1,6 @@
1
- # frozen_string_literal: false
1
+ # frozen_string_literal: true
2
2
  require 'haml/helpers'
3
+ require 'haml/preserver'
3
4
 
4
5
  # There are only helpers that depend on ActionView internals.
5
6
  module Haml
@@ -7,21 +8,12 @@ module Haml
7
8
  include Helpers
8
9
  extend self
9
10
 
10
- DEFAULT_PRESERVE_TAGS = %w[textarea pre code].freeze
11
+ DEFAULT_PRESERVE_TAGS = Preserver::DEFAULT_TAGS
11
12
 
12
13
  def find_and_preserve(input = nil, tags = DEFAULT_PRESERVE_TAGS, &block)
13
14
  return find_and_preserve(capture_haml(&block), input || tags) if block
14
15
 
15
- tags = tags.each_with_object('') do |t, s|
16
- s << '|' unless s.empty?
17
- s << Regexp.escape(t)
18
- end
19
-
20
- re = /<(#{tags})([^>]*)>(.*?)(<\/\1>)/im
21
- input.to_s.gsub(re) do |s|
22
- s =~ re # Can't rely on $1, etc. existing since Rails' SafeBuffer#gsub is incompatible
23
- "<#{$1}#{$2}>#{preserve($3)}</#{$1}>"
24
- end
16
+ Preserver.find_and_preserve(input, tags) { |content| preserve(content) }
25
17
  end
26
18
 
27
19
  def preserve(input = nil, &block)
@@ -10,7 +10,7 @@ module Haml
10
10
  def compile(code, node: RubyExpression.string_literal_node(code))
11
11
  case node
12
12
  when Prism::StringNode
13
- node.unescaped.empty? ? [] : [[:static, node.unescaped]]
13
+ node.unescaped.empty? ? [] : [[:static, in_source_encoding(node.unescaped, code)]]
14
14
  when Prism::InterpolatedStringNode
15
15
  compile_parts(node.parts, code)
16
16
  else
@@ -33,12 +33,12 @@ module Haml
33
33
  case part
34
34
  when Prism::StringNode
35
35
  content = part.unescaped
36
- exps << [:static, content] unless content.empty?
36
+ exps << [:static, in_source_encoding(content, code)] unless content.empty?
37
37
  when Prism::EmbeddedStatementsNode
38
38
  embedded = embedded_source(part, code)
39
39
  exps << [:dynamic, embedded] unless embedded.empty?
40
40
  when Prism::EmbeddedVariableNode
41
- exps << [:dynamic, part.variable.slice]
41
+ exps << [:dynamic, in_source_encoding(part.variable.slice, code)]
42
42
  else
43
43
  # Prism allows more part types than a string literal can currently hold. Dropping
44
44
  # one would silently lose content, so fail instead of rendering something wrong.
@@ -54,6 +54,17 @@ module Haml
54
54
  from = part.opening_loc.end_offset
55
55
  code.byteslice(from, part.closing_loc.start_offset - from)
56
56
  end
57
+
58
+ # Prism tags what it returns with the encoding it parsed `code` under (UTF-8 for
59
+ # a BINARY source), while the fragments Haml slices out of the source itself keep
60
+ # the source encoding. The compiled template mixes both kinds, so anything taken
61
+ # from Prism has to come back to the source encoding, or joining the fragments
62
+ # raises Encoding::CompatibilityError once both sides hold non-ASCII bytes.
63
+ def in_source_encoding(string, code)
64
+ return string if string.encoding == code.encoding
65
+
66
+ string.dup.force_encoding(code.encoding)
67
+ end
57
68
  end
58
69
 
59
70
  def on_dynamic(code)
data/lib/haml/template.rb CHANGED
@@ -16,5 +16,5 @@ module Haml
16
16
  "extend Haml::Helpers; #{super}"
17
17
  end
18
18
  end
19
- Template.send(:extend, TemplateExtension)
19
+ Template.extend TemplateExtension
20
20
  end
@@ -19,13 +19,27 @@ module Haml
19
19
  arg.count("\n") + cases.map do |cond, e|
20
20
  (cond == :else ? 0 : cond.count("\n")) + count_lines(e)
21
21
  end.reduce(:+)
22
- when :escape
22
+ when :escape, :fescape
23
23
  count_lines(args[1])
24
+ when :html
25
+ count_html_lines(args)
24
26
  when :newline
25
27
  1
26
28
  else
27
29
  raise UnexpectedExpression.new("[HAML BUG] Unexpected Temple expression '#{type}' is given!")
28
30
  end
29
31
  end
32
+
33
+ def self.count_html_lines(args)
34
+ case args[0]
35
+ when :attrs
36
+ args.drop(1).sum { |a| count_lines(a) }
37
+ when :attr
38
+ count_lines(args[2])
39
+ else
40
+ raise UnexpectedExpression.new("[HAML BUG] Unexpected Temple expression 'html #{args[0]}' is given!")
41
+ end
42
+ end
43
+ private_class_method :count_html_lines
30
44
  end
31
45
  end
data/lib/haml/util.rb CHANGED
@@ -5,7 +5,6 @@ begin
5
5
  rescue LoadError
6
6
  require 'erb'
7
7
  end
8
- require 'set'
9
8
  require 'stringio'
10
9
  require 'strscan'
11
10
 
@@ -26,10 +25,9 @@ module Haml
26
25
  end
27
26
  end
28
27
 
29
- # TODO: Remove unescape_interpolation's workaround and get rid of `respond_to?`.
30
28
  def self.escape_html_safe(html)
31
29
  html = html.to_s
32
- (html.respond_to?(:html_safe?) && html.html_safe?) ? html : escape_html(html)
30
+ html.html_safe? ? html : escape_html(html)
33
31
  end
34
32
 
35
33
  # Silence all output to STDERR within a block.
@@ -202,7 +200,7 @@ MSG
202
200
  /#[\{$@]/ === str
203
201
  end
204
202
 
205
- def unescape_interpolation(str, escape_html = nil)
203
+ def unescape_interpolation(str)
206
204
  res = ''.dup
207
205
  rest = Haml::Util.handle_interpolation str.dump do |scan|
208
206
  escapes = (scan[2].size - 1) / 2
@@ -218,12 +216,17 @@ MSG
218
216
  end
219
217
  content = eval("\"#{interpolated}\"")
220
218
  content = "#{char}#{content}" if char == '@' || char == '$'
221
- content = "Haml::Util.escape_html_safe((#{content}).to_s)" if escape_html
222
219
 
223
220
  res << "\#{#{content}}"
224
221
  end
225
222
  end
226
- res + rest
223
+ # The result is rebuilt from `str.dump` fragments and eval'd pieces, so it can
224
+ # come out tagged with an encoding that differs from `str`'s (e.g. UTF-8 for a
225
+ # BINARY source). The compiled template joins this string literal's fragments
226
+ # with fragments sliced out of the source itself, and mixed encodings raise
227
+ # Encoding::CompatibilityError, so bring it back to the source encoding.
228
+ # See haml/haml#1218.
229
+ (res + rest).force_encoding(str.encoding)
227
230
  end
228
231
 
229
232
  private
data/lib/haml/version.rb CHANGED
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module Haml
3
- VERSION = '7.3.0'
3
+ VERSION = '7.5.0'
4
4
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: haml
3
3
  version: !ruby/object:Gem::Version
4
- version: 7.3.0
4
+ version: 7.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Natalie Weizenbaum
@@ -307,6 +307,7 @@ files:
307
307
  - lib/haml/identity.rb
308
308
  - lib/haml/object_ref.rb
309
309
  - lib/haml/parser.rb
310
+ - lib/haml/preserver.rb
310
311
  - lib/haml/rails_helpers.rb
311
312
  - lib/haml/rails_template.rb
312
313
  - lib/haml/railtie.rb