haml 7.2.1 → 7.3.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: f4665a2a3c561765d35c0eeec78fdc87d2574cd640249089ace5035cc672e46f
4
- data.tar.gz: 02146e70c4ace1a58a1598ccfe25b08a22f06d0aa0554244806b1593cce85caa
3
+ metadata.gz: 381430279e5ca21c3d03f899c686f972609e54633c8e530609511e852597b7ef
4
+ data.tar.gz: a108268dccda6dc0e3ca014e484a7bcc0f50c973771a0057a086d75de2847495
5
5
  SHA512:
6
- metadata.gz: 3b24e11afe577f0b175fca4ae8fe5bdc45ae245e2f654a6d832ac2c619e25f2aeefb700e5ba51742f22ad5e849cc06d8831ef2a2c30083aab7a2bab727fcfe01
7
- data.tar.gz: ad77cc1be2d2831de2cbac2568efbc368fb1627626c19138e7059b120fb2e85c88057b8f9fb705c22f82b7d910497018e156acbc61691ce52db4de5d39a0e764
6
+ metadata.gz: b5f8d7bcc23e52be3fd4ed443ec3fdf171773f9c9aaacd1705d92ec09a3ef372adf13a8715ca4b4d039aa2a07fc2fdbb7e59eef30a22f55be2823bb0c7e23f0d
7
+ data.tar.gz: 954f3aa91fa3f49499d05dd1503056dd65a439519c5b2bcc300a6cf3dfbacf686717c8aa1e10ddb1ca7fa779504d92561a33e8a7d729b8013967cde102636fca
data/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # Haml Changelog
2
2
 
3
+ ## 7.3.0
4
+
5
+ * Replace Ripper with Prism https://github.com/haml/haml/pull/1214
6
+ * Interpolate `#@ivar`, `#$gvar` and `#@@cvar` in string literals instead of dropping them
7
+ * Keep an escaped delimiter of a percent literal, so `= %q{a\}b}` renders `a}b`
8
+ * Deprecate `Haml::AttributeParser.available?`, which is now always true and will be removed in the future
9
+
10
+ ## 7.2.2
11
+
12
+ * Remove obsolete TruffleRuby compatibility skips https://github.com/haml/haml/pull/1210
13
+
3
14
  ## 7.2.1
4
15
 
5
16
  * Do not rely on Ripper quirk in parsing old-style Haml attributes https://github.com/haml/haml/pull/1212
data/Gemfile CHANGED
@@ -20,7 +20,7 @@ if /java/.match?(RUBY_PLATFORM) # JRuby
20
20
  else
21
21
  gem 'redcarpet'
22
22
 
23
- if RUBY_PLATFORM !~ /mswin|mingw/ && RUBY_ENGINE != 'truffleruby'
23
+ if RUBY_PLATFORM !~ /mswin|mingw/
24
24
  gem 'stackprof'
25
25
  end
26
26
  end
data/haml.gemspec CHANGED
@@ -21,11 +21,14 @@ Gem::Specification.new do |spec|
21
21
 
22
22
  spec.metadata = { 'rubygems_mfa_required' => 'true' }
23
23
 
24
- spec.metadata["changelog_uri"] = "https://github.com/haml/haml/releases"
24
+ spec.metadata["bug_tracker_uri"] = "https://github.com/haml/haml/issues"
25
+ spec.metadata["changelog_uri"] = "https://github.com/haml/haml/blob/main/CHANGELOG.md"
26
+ spec.metadata["homepage_uri"] = "https://haml.info/"
25
27
  spec.metadata["source_code_uri"] = "https://github.com/haml/haml"
26
28
 
27
29
  spec.required_ruby_version = '>= 3.2.0'
28
30
 
31
+ spec.add_dependency 'prism', '>= 1.1.0'
29
32
  spec.add_dependency 'temple', '>= 0.8.2'
30
33
  spec.add_dependency 'thor'
31
34
  spec.add_dependency 'tilt'
@@ -22,9 +22,7 @@ module Haml
22
22
 
23
23
  def compile(node)
24
24
  hashes = []
25
- if node.value[:object_ref] != :nil || !AttributeParser.available?
26
- return runtime_compile(node)
27
- end
25
+ return runtime_compile(node) if node.value[:object_ref] != :nil
28
26
  [node.value[:dynamic_attributes].new, node.value[:dynamic_attributes].old].compact.each do |attribute_str|
29
27
  hash = AttributeParser.parse(attribute_str)
30
28
  return runtime_compile(node) if hash.nil? || hash.any? { |_key, value| value.empty? }
@@ -1,34 +1,40 @@
1
1
  # frozen_string_literal: true
2
- require 'haml/ruby_expression'
2
+ require 'prism'
3
3
 
4
4
  module Haml
5
5
  class AttributeParser
6
- class ParseSkip < StandardError
7
- end
8
-
9
- # @return [TrueClass, FalseClass] - return true if AttributeParser.parse can be used.
6
+ # @deprecated Prism is a hard dependency, so this is always true. Haml itself no longer
7
+ # asks, and this will be removed in the future.
8
+ # @return [TrueClass] - return true if AttributeParser.parse can be used.
10
9
  def self.available?
11
- # TruffleRuby doesn't have Ripper.lex
12
- defined?(Ripper) && Ripper.respond_to?(:lex) && Temple::StaticAnalyzer.available?
10
+ true
13
11
  end
14
12
 
15
13
  def self.parse(text)
16
14
  self.new.parse(text)
17
15
  end
18
16
 
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.
19
19
  def parse(text)
20
20
  exp = wrap_bracket(text)
21
- return if Temple::StaticAnalyzer.syntax_error?(exp)
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
+ node = hash_node(exp)
26
+ return if node.nil?
22
27
 
23
28
  hash = {}
24
- tokens = Ripper.lex(exp)[1..-2] || []
25
- each_attr(tokens) do |attr_tokens|
26
- key = parse_key!(attr_tokens)
27
- hash[key] = attr_tokens.map { |t| t[2] }.join.strip
29
+ node.elements.each do |element|
30
+ return unless element.is_a?(Prism::AssocNode)
31
+
32
+ key = static_key(element.key)
33
+ return if key.nil?
34
+
35
+ hash[key] = value_source(element.value)
28
36
  end
29
37
  hash
30
- rescue ParseSkip
31
- nil
32
38
  end
33
39
 
34
40
  private
@@ -39,78 +45,34 @@ module Haml
39
45
  "{#{text}}"
40
46
  end
41
47
 
42
- def parse_key!(tokens)
43
- _, type, str = tokens.shift
44
- case type
45
- when :on_sp
46
- parse_key!(tokens)
47
- when :on_label
48
- str.tr(':', '')
49
- when :on_symbeg
50
- _, _, key = tokens.shift
51
- assert_type!(tokens.shift, :on_tstring_end) if str != ':'
52
- skip_until_hash_rocket!(tokens)
53
- key
54
- when :on_tstring_beg
55
- _, _, key = tokens.shift
56
- next_token = tokens.shift
57
- unless next_token[1] == :on_label_end
58
- assert_type!(next_token, :on_tstring_end)
59
- skip_until_hash_rocket!(tokens)
60
- end
61
- key
62
- else
63
- raise ParseSkip
64
- end
65
- end
48
+ def hash_node(exp)
49
+ # partial_script, because an attribute may legitimately call `yield` and the like:
50
+ # a template is compiled into a method body.
51
+ result = Prism.parse(exp, partial_script: true)
52
+ return if result.failure?
53
+
54
+ statements = result.value.statements.body
55
+ return if statements.size != 1
66
56
 
67
- def assert_type!(token, type)
68
- raise ParseSkip if token[1] != type
57
+ node = statements.first
58
+ node if node.is_a?(Prism::HashNode)
69
59
  end
70
60
 
71
- def skip_until_hash_rocket!(tokens)
72
- until tokens.empty?
73
- _, type, str = tokens.shift
74
- break if type == :on_op && str == '=>'
61
+ # The key as written between its delimiters, not unescaped: an escape has to reach the
62
+ # attribute name as the source spelled it, like the `\0` of `{ "a\0b" => 1 }`.
63
+ def static_key(key)
64
+ case key
65
+ when Prism::SymbolNode then key.value_loc&.slice
66
+ when Prism::StringNode then key.content_loc&.slice
75
67
  end
76
68
  end
77
69
 
78
- def each_attr(tokens)
79
- attr_tokens = []
80
- open_tokens = Hash.new { |h, k| h[k] = 0 }
70
+ def value_source(value)
71
+ # Ruby 3.1 value omission (`{ foo: }`). An empty value tells AttributeCompiler to
72
+ # fall back to the runtime, which is what resolves it.
73
+ return '' if value.is_a?(Prism::ImplicitNode)
81
74
 
82
- tokens.each do |token|
83
- _, type, _ = token
84
- case type
85
- when :on_comma
86
- if open_tokens.values.all?(&:zero?)
87
- yield(attr_tokens)
88
- attr_tokens = []
89
- next
90
- end
91
- when :on_lbracket
92
- open_tokens[:array] += 1
93
- when :on_rbracket
94
- open_tokens[:array] -= 1
95
- when :on_lbrace
96
- open_tokens[:block] += 1
97
- when :on_rbrace
98
- open_tokens[:block] -= 1
99
- when :on_lparen
100
- open_tokens[:paren] += 1
101
- when :on_rparen
102
- open_tokens[:paren] -= 1
103
- when :on_embexpr_beg
104
- open_tokens[:embexpr] += 1
105
- when :on_embexpr_end
106
- open_tokens[:embexpr] -= 1
107
- when :on_sp
108
- next if attr_tokens.empty?
109
- end
110
-
111
- attr_tokens << token
112
- end
113
- yield(attr_tokens) unless attr_tokens.empty?
75
+ value.slice
114
76
  end
115
77
  end
116
78
  end
@@ -21,10 +21,6 @@ module Haml
21
21
  end
22
22
 
23
23
  def compile(node, &block)
24
- unless Ripper.respond_to?(:lex) # No Ripper.lex in truffleruby
25
- return dynamic_compile(node, &block)
26
- end
27
-
28
24
  no_children = node.children.empty?
29
25
  case
30
26
  when no_children && node.value[:escape_interpolation]
@@ -43,8 +39,13 @@ module Haml
43
39
  # String-interpolated plain text must be compiled with this method
44
40
  # because we have to escape only interpolated values.
45
41
  def compile_interpolated_plain(node)
42
+ compiled = StringSplitter.try_compile(node.value[:text])
43
+ # Ruby that does not parse: let the generated code report it, rather than failing
44
+ # the whole compilation with an error that points at Haml instead of the template.
45
+ return delegate_optimization(node) if compiled.nil?
46
+
46
47
  temple = [:multi]
47
- StringSplitter.compile(node.value[:text]).each do |type, value|
48
+ compiled.each do |type, value|
48
49
  case type
49
50
  when :static
50
51
  temple << [:static, value]
@@ -28,10 +28,8 @@ module Haml
28
28
  nil
29
29
  when node.value[:parse]
30
30
  return compile_interpolated_plain(node) if node.value[:escape_interpolation]
31
- if Ripper.respond_to?(:lex) # No Ripper.lex in truffleruby
32
- return delegate_optimization(node) if RubyExpression.string_literal?(node.value[:value])
33
- return delegate_optimization(node) if Temple::StaticAnalyzer.static?(node.value[:value])
34
- end
31
+ return delegate_optimization(node) if RubyExpression.string_literal?(node.value[:value])
32
+ return delegate_optimization(node) if Temple::StaticAnalyzer.static?(node.value[:value])
35
33
 
36
34
  var = @identity.generate
37
35
  [:multi,
@@ -55,8 +53,13 @@ module Haml
55
53
 
56
54
  # We should handle interpolation here to escape only interpolated values.
57
55
  def compile_interpolated_plain(node)
56
+ compiled = StringSplitter.try_compile(node.value[:value])
57
+ # Ruby that does not parse: let the generated code report it, rather than failing
58
+ # the whole compilation with an error that points at Haml instead of the template.
59
+ return delegate_optimization(node) if compiled.nil?
60
+
58
61
  temple = [:multi]
59
- StringSplitter.compile(node.value[:value]).each do |type, value|
62
+ compiled.each do |type, value|
60
63
  case type
61
64
  when :static
62
65
  temple << [:static, value]
@@ -14,7 +14,12 @@ module Haml
14
14
 
15
15
  def compile_plain(text)
16
16
  string_literal = ::Haml::Util.unescape_interpolation(text)
17
- StringSplitter.compile(string_literal).map do |temple|
17
+ compiled = StringSplitter.try_compile(string_literal)
18
+ # Ruby that does not parse: let the generated code report it against the template,
19
+ # rather than failing the whole compilation.
20
+ return [[:escape, false, [:dynamic, string_literal]]] if compiled.nil?
21
+
22
+ compiled.map do |temple|
18
23
  type, str = temple
19
24
  case type
20
25
  when :dynamic
data/lib/haml/parser.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'ripper'
3
+ require 'prism'
4
4
  require 'strscan'
5
5
  require 'haml/error'
6
6
  require 'haml/util'
@@ -601,8 +601,7 @@ module Haml
601
601
  attributes
602
602
  end
603
603
 
604
- # This method doesn't use Haml::HamlAttributeParser because currently it depends on Ripper and Rubinius doesn't provide it.
605
- # Ideally this logic should be placed in Haml::HamlAttributeParser instead of here and this method should use it.
604
+ # Ideally this logic should be placed in Haml::AttributeParser instead of here and this method should use it.
606
605
  #
607
606
  # @param [String] text - Hash literal or text inside old attributes
608
607
  # @return [Hash,nil] - Return nil if text is not static Hash literal
@@ -686,17 +685,17 @@ module Haml
686
685
  # Old attributes often look like a valid Hash literal, but it sometimes allow code like
687
686
  # `{ hash, foo: bar }`, which is compiled to `_hamlout.attributes({}, nil, hash, foo: bar)`.
688
687
  #
689
- # To scan such code correctly, this scans `a( hash, foo: bar }` instead, stops when there is
690
- # 1 more :on_embexpr_end (the last '}') than :on_embexpr_beg, and resurrects '{' afterwards.
688
+ # To scan such code correctly, this scans `a( hash, foo: bar }` instead, stops on the '}'
689
+ # that closes one more brace than was opened, and resurrects '{' afterwards.
691
690
  #
692
691
  # Old attributes allow invalid Hash constructs (e.g., `{ hash, foo: bar }`).
693
692
  # Replacing the opening `{` with `a(` makes the syntax look like a method call so
694
- # Ripper can lex it reliably.
693
+ # it can be lexed reliably.
695
694
 
696
695
  attributes_hash, rest = balance_tokens(
697
696
  text.sub(?{, METHOD_CALL_PREFIX),
698
- [:on_lbrace, :on_tlambeg, :on_embexpr_beg],
699
- [:on_rbrace, :on_embexpr_end],
697
+ [:BRACE_LEFT, :LAMBDA_BEGIN, :EMBEXPR_BEGIN],
698
+ [:BRACE_RIGHT, :EMBEXPR_END],
700
699
  count: 1)
701
700
  attributes_hash = attributes_hash.sub(METHOD_CALL_PREFIX, ?{)
702
701
  rescue SyntaxError => e
@@ -854,19 +853,23 @@ module Haml
854
853
  Haml::Util.balance(*args) or raise(SyntaxError.new(Error.message(:unbalanced_brackets)))
855
854
  end
856
855
 
857
- # Unlike #balance, this balances Ripper tokens to balance something like `{ a: "}" }` correctly.
856
+ # Unlike #balance, this balances lexed tokens to balance something like `{ a: "}" }` correctly.
858
857
  def balance_tokens(buf, start, finish, count: 0)
859
- text = ''.dup
860
- Ripper.lex(buf).each do |_, token, str|
861
- text << str
862
- if start.include?(token)
858
+ Prism.lex(buf).value.each do |token, _|
859
+ type = token.type
860
+ next if type == :EOF
861
+
862
+ if start.include?(type)
863
863
  count += 1
864
- elsif finish.include?(token)
864
+ elsif finish.include?(type)
865
865
  count -= 1
866
866
  end
867
867
 
868
868
  if count == 0
869
- return text, buf.sub(text, '')
869
+ # Splitting on the offset instead of the tokens seen so far: whitespace and comments
870
+ # are not tokens, so the text cannot be rebuilt by concatenating them.
871
+ offset = token.location.end_offset
872
+ return buf.byteslice(0, offset), buf.byteslice(offset..)
870
873
  end
871
874
  end
872
875
  raise SyntaxError.new(Error.message(:unbalanced_brackets))
@@ -1,32 +1,39 @@
1
1
  # frozen_string_literal: true
2
- require 'ripper'
2
+ require 'prism'
3
3
 
4
4
  module Haml
5
- class RubyExpression < Ripper
6
- class ParseError < StandardError; end
5
+ class RubyExpression
6
+ # A character literal (`?a`) is a StringNode for Prism, but it has no quotes
7
+ # to split on, so it must not be reported as a string literal.
8
+ CHAR_LITERAL_OPENING = '?'
9
+
10
+ # A template is compiled into a method body, so a jump like `yield` is valid here even
11
+ # though it would not be at the top level of a script.
12
+ PARSE_OPTIONS = { partial_script: true }.freeze
7
13
 
8
14
  def self.syntax_error?(code)
9
- self.new(code).parse
10
- false
11
- rescue ParseError
12
- true
15
+ Prism.parse_failure?(code, **PARSE_OPTIONS)
13
16
  end
14
17
 
15
18
  def self.string_literal?(code)
16
- return false if syntax_error?(code)
17
-
18
- type, instructions = Ripper.sexp(code)
19
- return false if type != :program
20
- return false if instructions.size > 1
21
-
22
- type, _ = instructions.first
23
- type == :string_literal
19
+ !string_literal_node(code).nil?
24
20
  end
25
21
 
26
- private
22
+ # @return [Prism::Node, nil] - the node of a string literal StringSplitter can split, if any.
23
+ # Its locations are byte offsets into `code` as given, so `code` must not be stripped here.
24
+ def self.string_literal_node(code)
25
+ result = Prism.parse(code, **PARSE_OPTIONS)
26
+ return if result.failure?
27
+
28
+ statements = result.value.statements.body
29
+ return if statements.size > 1
27
30
 
28
- def on_parse_error(*)
29
- raise ParseError
31
+ case (node = statements.first)
32
+ when Prism::StringNode, Prism::InterpolatedStringNode
33
+ # Adjacent concatenation (`"a" "b"`) is one node without an opening
34
+ # delimiter of its own, and each of its parts keeps its own quotes.
35
+ node if !node.opening.nil? && node.opening != CHAR_LITERAL_OPENING
36
+ end
30
37
  end
31
38
  end
32
39
  end
@@ -1,140 +1,77 @@
1
1
  # frozen_string_literal: true
2
- begin
3
- require 'ripper'
4
- rescue LoadError
5
- end
2
+ require 'prism'
3
+ require 'haml/ruby_expression'
6
4
 
7
5
  module Haml
8
6
  # Compile [:dynamic, "foo#{bar}"] to [:multi, [:static, 'foo'], [:dynamic, 'bar']]
9
7
  class StringSplitter < Temple::Filter
10
- if defined?(Ripper) && Ripper.respond_to?(:lex)
11
- class << self
12
- # `code` param must be valid string literal
13
- def compile(code)
14
- [].tap do |exps|
15
- tokens = Ripper.lex(code.strip)
16
- tokens.pop while tokens.last && [:on_comment, :on_sp].include?(tokens.last[1])
17
-
18
- if tokens.size < 2
19
- raise(Haml::InternalError, "Expected token size >= 2 but got: #{tokens.size}")
20
- end
21
- compile_tokens!(exps, tokens)
22
- end
23
- end
24
-
25
- private
26
-
27
- def strip_quotes!(tokens)
28
- _, type, beg_str = tokens.shift
29
- if type != :on_tstring_beg
30
- raise(Haml::InternalError, "Expected :on_tstring_beg but got: #{type}")
31
- end
32
-
33
- _, type, end_str = tokens.pop
34
- if type != :on_tstring_end
35
- raise(Haml::InternalError, "Expected :on_tstring_end but got: #{type}")
36
- end
37
-
38
- [beg_str, end_str]
8
+ class << self
9
+ # `code` param must be a string literal, as RubyExpression.string_literal? defines it.
10
+ def compile(code, node: RubyExpression.string_literal_node(code))
11
+ case node
12
+ when Prism::StringNode
13
+ node.unescaped.empty? ? [] : [[:static, node.unescaped]]
14
+ when Prism::InterpolatedStringNode
15
+ compile_parts(node.parts, code)
16
+ else
17
+ raise(Haml::InternalError, "Expected a string literal but got: #{code}")
39
18
  end
19
+ end
40
20
 
41
- def compile_tokens!(exps, tokens)
42
- beg_str, end_str = strip_quotes!(tokens)
21
+ # Like compile, but nil instead of raising, for callers that built `code` themselves
22
+ # and would rather emit it untouched than fail the whole compilation.
23
+ def try_compile(code)
24
+ node = RubyExpression.string_literal_node(code)
25
+ compile(code, node: node) unless node.nil?
26
+ end
43
27
 
44
- until tokens.empty?
45
- _, type, str = tokens.shift
28
+ private
46
29
 
47
- case type
48
- when :on_tstring_content
49
- beg_str, end_str = escape_quotes(beg_str, end_str)
50
- exps << [:static, eval("#{beg_str}#{str}#{end_str}").to_s]
51
- when :on_embexpr_beg
52
- embedded = shift_balanced_embexpr(tokens)
30
+ def compile_parts(parts, code)
31
+ [].tap do |exps|
32
+ parts.each do |part|
33
+ case part
34
+ when Prism::StringNode
35
+ content = part.unescaped
36
+ exps << [:static, content] unless content.empty?
37
+ when Prism::EmbeddedStatementsNode
38
+ embedded = embedded_source(part, code)
53
39
  exps << [:dynamic, embedded] unless embedded.empty?
40
+ when Prism::EmbeddedVariableNode
41
+ exps << [:dynamic, part.variable.slice]
42
+ else
43
+ # Prism allows more part types than a string literal can currently hold. Dropping
44
+ # one would silently lose content, so fail instead of rendering something wrong.
45
+ raise(Haml::InternalError, "Unexpected #{part.class} in string literal: #{code}")
54
46
  end
55
47
  end
56
48
  end
57
-
58
- # Some quotes are split-unsafe. Replace such quotes with null characters.
59
- def escape_quotes(beg_str, end_str)
60
- case [beg_str[-1], end_str]
61
- when ['(', ')'], ['[', ']'], ['{', '}']
62
- [beg_str.sub(/.\z/) { "\0" }, "\0"]
63
- else
64
- [beg_str, end_str]
65
- end
66
- end
67
-
68
- def shift_balanced_embexpr(tokens)
69
- String.new.tap do |embedded|
70
- embexpr_open = 1
71
-
72
- until tokens.empty?
73
- _, type, str = tokens.shift
74
- case type
75
- when :on_embexpr_beg
76
- embexpr_open += 1
77
- when :on_embexpr_end
78
- embexpr_open -= 1
79
- break if embexpr_open == 0
80
- end
81
-
82
- embedded << str
83
- end
84
- end
85
- end
86
- end
87
-
88
- def on_dynamic(code)
89
- return [:dynamic, code] unless string_literal?(code)
90
- return [:dynamic, code] if code.include?("\n")
91
-
92
- temple = [:multi]
93
- StringSplitter.compile(code).each do |type, content|
94
- case type
95
- when :static
96
- temple << [:static, content]
97
- when :dynamic
98
- temple << on_dynamic(content)
99
- end
100
- end
101
- temple
102
49
  end
103
50
 
104
- private
105
-
106
- def string_literal?(code)
107
- return false if SyntaxChecker.syntax_error?(code)
108
-
109
- type, instructions = Ripper.sexp(code)
110
- return false if type != :program
111
- return false if instructions.size > 1
112
-
113
- type, _ = instructions.first
114
- type == :string_literal
51
+ # The source between `#{` and `}`, kept verbatim. Slicing `code` rather than
52
+ # using the statements' own source preserves the whitespace around them.
53
+ def embedded_source(part, code)
54
+ from = part.opening_loc.end_offset
55
+ code.byteslice(from, part.closing_loc.start_offset - from)
115
56
  end
57
+ end
116
58
 
117
- class SyntaxChecker < Ripper
118
- class ParseError < StandardError; end
119
-
120
- def self.syntax_error?(code)
121
- self.new(code).parse
122
- false
123
- rescue ParseError
124
- true
125
- end
59
+ def on_dynamic(code)
60
+ return [:dynamic, code] if code.include?("\n")
126
61
 
127
- private
62
+ node = RubyExpression.string_literal_node(code)
63
+ return [:dynamic, code] if node.nil?
128
64
 
129
- def on_parse_error(*)
130
- raise ParseError
65
+ temple = [:multi]
66
+ StringSplitter.compile(code, node: node).each do |type, content|
67
+ case type
68
+ when :static
69
+ temple << [:static, content]
70
+ when :dynamic
71
+ temple << on_dynamic(content)
131
72
  end
132
73
  end
133
- else
134
- # Do nothing if ripper is unavailable
135
- def call(ast)
136
- ast
137
- end
74
+ temple
138
75
  end
139
76
  end
140
77
  end
data/lib/haml/version.rb CHANGED
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module Haml
3
- VERSION = '7.2.1'
3
+ VERSION = '7.3.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.2.1
4
+ version: 7.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Natalie Weizenbaum
@@ -13,6 +13,20 @@ bindir: exe
13
13
  cert_chain: []
14
14
  date: 1980-01-02 00:00:00.000000000 Z
15
15
  dependencies:
16
+ - !ruby/object:Gem::Dependency
17
+ name: prism
18
+ requirement: !ruby/object:Gem::Requirement
19
+ requirements:
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 1.1.0
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: 1.1.0
16
30
  - !ruby/object:Gem::Dependency
17
31
  name: temple
18
32
  requirement: !ruby/object:Gem::Requirement
@@ -308,7 +322,9 @@ licenses:
308
322
  - MIT
309
323
  metadata:
310
324
  rubygems_mfa_required: 'true'
311
- changelog_uri: https://github.com/haml/haml/releases
325
+ bug_tracker_uri: https://github.com/haml/haml/issues
326
+ changelog_uri: https://github.com/haml/haml/blob/main/CHANGELOG.md
327
+ homepage_uri: https://haml.info/
312
328
  source_code_uri: https://github.com/haml/haml
313
329
  rdoc_options: []
314
330
  require_paths: