haml 7.3.0 → 7.4.1

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: 4dced0d7329423afd97a56b1ce6b32f4c332a9f4e725aed6a41f4e5d7c10bfd7
4
+ data.tar.gz: 985f140fb9f9ae590c0e37ae7d5f06632c5c1f4a522578639e2a98527fb85e20
5
5
  SHA512:
6
- metadata.gz: b5f8d7bcc23e52be3fd4ed443ec3fdf171773f9c9aaacd1705d92ec09a3ef372adf13a8715ca4b4d039aa2a07fc2fdbb7e59eef30a22f55be2823bb0c7e23f0d
7
- data.tar.gz: 954f3aa91fa3f49499d05dd1503056dd65a439519c5b2bcc300a6cf3dfbacf686717c8aa1e10ddb1ca7fa779504d92561a33e8a7d729b8013967cde102636fca
6
+ metadata.gz: ba4cc15a925752648e293fd712e024c92da5d0e1d827e28719e871e5ffa7a647fc928c46650f7a2c92bf3ad8c67e70f821fd1710de346affb528639f34652af1
7
+ data.tar.gz: 062d2e3a5b523a5450179790f20627920e9086065b13f15591b4472178a3e7ec4672caa299466a428b343a21b5fd7eb85c0549f032b22ba5854aacf1510ad576
data/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Haml Changelog
2
2
 
3
+ ## 7.4.1
4
+
5
+ * Build the preserve regex once per tag list instead of per call https://github.com/haml/haml/pull/1220
6
+
7
+ ## 7.4.0
8
+
9
+ * `Haml::BOOLEAN_ATTRIBUTES` is changed from Array to Set https://github.com/haml/haml/pull/1216
10
+ * A guard on `respond_to?(:html_safe?)` is removed from `Haml::Util.escape_html_safe` https://github.com/haml/haml/pull/1217
11
+ * Keep interpolated text in the source encoding, fixing the `Encoding::CompatibilityError` remaining on an ASCII-8BIT
12
+ template source that mixes plain text and interpolation with non-ASCII characters https://github.com/haml/haml/issues/1218
13
+
14
+ ## 7.3.1
15
+
16
+ * Keep Prism-derived fragments in the source encoding, fixing `Encoding::CompatibilityError` on an ASCII-8BIT
17
+ template source with non-ASCII characters (regression in 7.3.0) https://github.com/haml/haml/issues/1218
18
+
3
19
  ## 7.3.0
4
20
 
5
21
  * 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,17 @@
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'
5
6
 
6
7
  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
8
+ # The set of boolean attributes. You may add custom attributes to this constant.
9
+ BOOLEAN_ATTRIBUTES = Set.new(%w[disabled readonly multiple checked autobuffer
9
10
  autoplay controls loop selected hidden scoped async
10
11
  defer reversed ismap seamless muted required
11
12
  autofocus novalidate formnovalidate open pubdate
12
13
  itemscope allowfullscreen default inert sortable
13
- truespeed typemustmatch download]
14
+ truespeed typemustmatch download])
14
15
 
15
16
  class AttributeCompiler
16
17
  def initialize(identity, options)
@@ -55,10 +56,12 @@ module Haml
55
56
  compile_class!(temple, key, values)
56
57
  when 'data', 'aria'
57
58
  compile_data!(temple, key, values)
58
- when *BOOLEAN_ATTRIBUTES, /\Adata-/, /\Aaria-/
59
- compile_boolean!(temple, key, values)
60
59
  else
61
- compile_common!(temple, key, values)
60
+ if BOOLEAN_ATTRIBUTES.include?(key) || key.start_with?('data-', 'aria-')
61
+ compile_boolean!(temple, key, values)
62
+ else
63
+ compile_common!(temple, key, values)
64
+ end
62
65
  end
63
66
  end
64
67
  temple
@@ -32,7 +32,7 @@ module Haml
32
32
  key = static_key(element.key)
33
33
  return if key.nil?
34
34
 
35
- hash[key] = value_source(element.value)
35
+ hash[in_source_encoding(key, exp)] = in_source_encoding(value_source(element.value), exp)
36
36
  end
37
37
  hash
38
38
  end
@@ -74,5 +74,15 @@ module Haml
74
74
 
75
75
  value.slice
76
76
  end
77
+
78
+ # Prism tags its slices with the encoding it parsed the source under (UTF-8 for a
79
+ # BINARY source), while the rest of the compiled template stays in the source
80
+ # encoding. Mixing the two raises Encoding::CompatibilityError once both sides hold
81
+ # non-ASCII bytes, so bring everything back to the source encoding.
82
+ def in_source_encoding(string, source)
83
+ return string if string.encoding == source.encoding
84
+
85
+ string.dup.force_encoding(source.encoding)
86
+ end
77
87
  end
78
88
  end
@@ -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
@@ -444,7 +444,8 @@ module Haml
444
444
  end
445
445
  else
446
446
  if Util.contains_interpolation?(value)
447
- value = Util.unescape_interpolation(value, escape_html)
447
+ value = Util.unescape_interpolation(value)
448
+ escape_interpolation = true if escape_html
448
449
  parse = true
449
450
  escape_html = false
450
451
  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/util.rb CHANGED
@@ -26,10 +26,9 @@ module Haml
26
26
  end
27
27
  end
28
28
 
29
- # TODO: Remove unescape_interpolation's workaround and get rid of `respond_to?`.
30
29
  def self.escape_html_safe(html)
31
30
  html = html.to_s
32
- (html.respond_to?(:html_safe?) && html.html_safe?) ? html : escape_html(html)
31
+ html.html_safe? ? html : escape_html(html)
33
32
  end
34
33
 
35
34
  # Silence all output to STDERR within a block.
@@ -202,7 +201,7 @@ MSG
202
201
  /#[\{$@]/ === str
203
202
  end
204
203
 
205
- def unescape_interpolation(str, escape_html = nil)
204
+ def unescape_interpolation(str)
206
205
  res = ''.dup
207
206
  rest = Haml::Util.handle_interpolation str.dump do |scan|
208
207
  escapes = (scan[2].size - 1) / 2
@@ -218,12 +217,17 @@ MSG
218
217
  end
219
218
  content = eval("\"#{interpolated}\"")
220
219
  content = "#{char}#{content}" if char == '@' || char == '$'
221
- content = "Haml::Util.escape_html_safe((#{content}).to_s)" if escape_html
222
220
 
223
221
  res << "\#{#{content}}"
224
222
  end
225
223
  end
226
- res + rest
224
+ # The result is rebuilt from `str.dump` fragments and eval'd pieces, so it can
225
+ # come out tagged with an encoding that differs from `str`'s (e.g. UTF-8 for a
226
+ # BINARY source). The compiled template joins this string literal's fragments
227
+ # with fragments sliced out of the source itself, and mixed encodings raise
228
+ # Encoding::CompatibilityError, so bring it back to the source encoding.
229
+ # See haml/haml#1218.
230
+ (res + rest).force_encoding(str.encoding)
227
231
  end
228
232
 
229
233
  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.4.1'
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.4.1
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