haml 6.4.0 → 7.5.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.
data/lib/haml/engine.rb CHANGED
@@ -17,13 +17,13 @@ module Haml
17
17
  :buffer_class,
18
18
  generator: Temple::Generators::StringBuffer,
19
19
  format: :html,
20
- attr_quote: "'",
20
+ attr_quote: '"',
21
21
  escape_html: true,
22
22
  escape_attrs: true,
23
23
  autoclose: %w(area base basefont br col command embed frame
24
24
  hr img input isindex keygen link menuitem meta
25
25
  param source track wbr),
26
- filename: "",
26
+ filename: '',
27
27
  disable_capture: false,
28
28
  remove_whitespace: false,
29
29
  )
@@ -53,7 +53,7 @@ module Haml
53
53
  end
54
54
 
55
55
  def precompiled_with_ambles(_local_names, after_preamble:)
56
- "#{after_preamble.tr("\n", ';')}#{@precompiled}".dup
56
+ "#{after_preamble.tr("\n", ';')}#{@precompiled}"
57
57
  end
58
58
  end
59
59
  end
@@ -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/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/, '
')
6
+ s.gsub!("\n", '
')
7
7
  s.delete!("\r")
8
8
  s
9
9
  end
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'
@@ -182,7 +182,7 @@ module Haml
182
182
 
183
183
  tabs = line.whitespace.length / @indentation.length
184
184
  return tabs if line.whitespace == @indentation * tabs
185
- return @template_tabs + 1 if flat? && line.whitespace =~ /^#{@flat_spaces}/
185
+ return @template_tabs + 1 if flat? && /^#{@flat_spaces}/.match?(line.whitespace)
186
186
 
187
187
  message = Error.message(:inconsistent_indentation,
188
188
  human_indentation(line.whitespace),
@@ -227,7 +227,7 @@ module Haml
227
227
  end
228
228
 
229
229
  def inspect
230
- %Q[(#{type} #{value.inspect}#{children.each_with_object(''.dup) {|c, s| s << "\n#{c.inspect.gsub!(/^/, ' ')}"}})].dup
230
+ %Q[(#{type} #{value.inspect}#{children.each_with_object(+'') {|c, s| s << "\n#{c.inspect.gsub!(/^/, ' ')}"}})]
231
231
  end
232
232
  end
233
233
 
@@ -236,7 +236,7 @@ module Haml
236
236
  DynamicAttributes = Struct.new(:new, :old) do
237
237
  undef :old=
238
238
  def old=(value)
239
- unless value =~ /\A{.*}\z/m
239
+ unless /\A{.*}\z/m.match?(value)
240
240
  raise ArgumentError.new('Old attributes must start with "{" and end with "}"')
241
241
  end
242
242
  self[:old] = value
@@ -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.
@@ -397,7 +403,7 @@ module Haml
397
403
  def haml_comment(text)
398
404
  if filter_opened?
399
405
  @flat = true
400
- @filter_buffer = String.new
406
+ @filter_buffer = +''
401
407
  @filter_buffer << "#{text}\n" unless text.empty?
402
408
  text = @filter_buffer
403
409
  # If we don't know the indentation by now, it'll be set in Line#tabs
@@ -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
@@ -528,11 +535,11 @@ module Haml
528
535
  end
529
536
 
530
537
  def filter(name)
531
- raise Error.new(Error.message(:invalid_filter_name, name)) unless name =~ /^\w+$/
538
+ raise Error.new(Error.message(:invalid_filter_name, name)) unless /^\w+$/.match?(name)
532
539
 
533
540
  if filter_opened?
534
541
  @flat = true
535
- @filter_buffer = String.new
542
+ @filter_buffer = +''
536
543
  # If we don't know the indentation by now, it'll be set in Line#tabs
537
544
  @flat_spaces = @indentation * (@template_tabs+1) if @indentation
538
545
  end
@@ -601,8 +608,7 @@ module Haml
601
608
  attributes
602
609
  end
603
610
 
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.
611
+ # Ideally this logic should be placed in Haml::AttributeParser instead of here and this method should use it.
606
612
  #
607
613
  # @param [String] text - Hash literal or text inside old attributes
608
614
  # @return [Hash,nil] - Return nil if text is not static Hash literal
@@ -630,7 +636,7 @@ module Haml
630
636
 
631
637
  tag_name, attributes, rest = match
632
638
 
633
- if !attributes.empty? && (attributes =~ /[.#](\.|#|\z)/)
639
+ if !attributes.empty? && /[.#](\.|#|\z)/.match?(attributes)
634
640
  raise SyntaxError.new(Error.message(:illegal_element))
635
641
  end
636
642
 
@@ -686,10 +692,19 @@ module Haml
686
692
  # Old attributes often look like a valid Hash literal, but it sometimes allow code like
687
693
  # `{ hash, foo: bar }`, which is compiled to `_hamlout.attributes({}, nil, hash, foo: bar)`.
688
694
  #
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.
691
- balanced, rest = balance_tokens(text.sub(?{, METHOD_CALL_PREFIX), :on_embexpr_beg, :on_embexpr_end, count: 1)
692
- attributes_hash = balanced.sub(METHOD_CALL_PREFIX, ?{)
695
+ # To scan such code correctly, this scans `a( hash, foo: bar }` instead, stops on the '}'
696
+ # that closes one more brace than was opened, and resurrects '{' afterwards.
697
+ #
698
+ # Old attributes allow invalid Hash constructs (e.g., `{ hash, foo: bar }`).
699
+ # Replacing the opening `{` with `a(` makes the syntax look like a method call so
700
+ # it can be lexed reliably.
701
+
702
+ attributes_hash, rest = balance_tokens(
703
+ text.sub(?{, METHOD_CALL_PREFIX),
704
+ [:BRACE_LEFT, :LAMBDA_BEGIN, :EMBEXPR_BEGIN],
705
+ [:BRACE_RIGHT, :EMBEXPR_END],
706
+ count: 1)
707
+ attributes_hash = attributes_hash.sub(METHOD_CALL_PREFIX, ?{)
693
708
  rescue SyntaxError => e
694
709
  if e.message == Error.message(:unbalanced_brackets) && !@template.empty?
695
710
  text << "\n#{@next_line.text}"
@@ -734,7 +749,7 @@ module Haml
734
749
  end
735
750
 
736
751
  static_attributes = {}
737
- dynamic_attributes = "{".dup
752
+ dynamic_attributes = +"{"
738
753
  attributes.each do |name, (type, val)|
739
754
  if type == :static
740
755
  static_attributes[name] = val
@@ -774,7 +789,7 @@ module Haml
774
789
 
775
790
  return name, [:static, content.first[1]] if content.size == 1
776
791
  return name, [:dynamic,
777
- %!"#{content.each_with_object(''.dup) {|(t, v), s| s << (t == :str ? Util.inspect_obj(v)[1...-1] : "\#{#{v}}")}}"!]
792
+ %!"#{content.each_with_object(+'') {|(t, v), s| s << (t == :str ? Util.inspect_obj(v)[1...-1] : "\#{#{v}}")}}"!]
778
793
  end
779
794
 
780
795
  def next_line
@@ -786,7 +801,7 @@ module Haml
786
801
  line_defined = instance_variable_defined?(:@line)
787
802
  @line.tabs if line_defined
788
803
  unless (flat? && !closes_flat?(line) && !closes_flat?(@line)) ||
789
- (line_defined && @line.text[0] == ?: && line.full =~ %r[^#{@line.full[/^\s+/]}\s])
804
+ (line_defined && @line.text[0] == ?: && %r[^#{@line.full[/^\s+/]}\s].match?(line.full))
790
805
  return next_line if line.text.empty?
791
806
 
792
807
  handle_multiline(line)
@@ -796,7 +811,7 @@ module Haml
796
811
  end
797
812
 
798
813
  def closes_flat?(line)
799
- line && !line.text.empty? && line.full !~ /^#{@flat_spaces}/
814
+ line && !line.text.empty? && !(/^#{@flat_spaces}/.match?(line.full))
800
815
  end
801
816
 
802
817
  def handle_multiline(line)
@@ -814,7 +829,7 @@ module Haml
814
829
 
815
830
  # Checks whether or not `line` is in a multiline sequence.
816
831
  def is_multiline?(text)
817
- text && text.length > 1 && text[-1] == MULTILINE_CHAR_VALUE && text[-2] == ?\s && text !~ BLOCK_WITH_SPACES
832
+ text && text.length > 1 && text[-1] == MULTILINE_CHAR_VALUE && text[-2] == ?\s && !BLOCK_WITH_SPACES.match?(text)
818
833
  end
819
834
 
820
835
  def handle_ruby_multiline(line)
@@ -838,27 +853,30 @@ module Haml
838
853
  # - and not "?\," which is a character literal
839
854
  def is_ruby_multiline?(text)
840
855
  text && text.length > 1 && text[-1] == ?, &&
841
- !((text[-3, 2] =~ /\W\?/) || text[-3, 2] == "?\\")
856
+ !(/\W\?/.match?(text[-3, 2]) || text[-3, 2] == "?\\")
842
857
  end
843
858
 
844
859
  def balance(*args)
845
860
  Haml::Util.balance(*args) or raise(SyntaxError.new(Error.message(:unbalanced_brackets)))
846
861
  end
847
862
 
848
- # Unlike #balance, this balances Ripper tokens to balance something like `{ a: "}" }` correctly.
863
+ # Unlike #balance, this balances lexed tokens to balance something like `{ a: "}" }` correctly.
849
864
  def balance_tokens(buf, start, finish, count: 0)
850
- text = ''.dup
851
- Ripper.lex(buf).each do |_, token, str|
852
- text << str
853
- case token
854
- when start
865
+ Prism.lex(buf).value.each do |token, _|
866
+ type = token.type
867
+ next if type == :EOF
868
+
869
+ if start.include?(type)
855
870
  count += 1
856
- when finish
871
+ elsif finish.include?(type)
857
872
  count -= 1
858
873
  end
859
874
 
860
875
  if count == 0
861
- return text, buf.sub(text, '')
876
+ # Splitting on the offset instead of the tokens seen so far: whitespace and comments
877
+ # are not tokens, so the text cannot be rebuilt by concatenating them.
878
+ offset = token.location.end_offset
879
+ return buf.byteslice(0, offset), buf.byteslice(offset..)
862
880
  end
863
881
  end
864
882
  raise SyntaxError.new(Error.message(:unbalanced_brackets))
@@ -871,7 +889,7 @@ module Haml
871
889
  # Same semantics as block_opened?, except that block_opened? uses Line#tabs,
872
890
  # which doesn't interact well with filter lines
873
891
  def filter_opened?
874
- @next_line.full =~ (@indentation ? /^#{@indentation * (@template_tabs + 1)}/ : /^\s/)
892
+ (@indentation ? /^#{@indentation * (@template_tabs + 1)}/ : /^\s/).match?(@next_line.full)
875
893
  end
876
894
 
877
895
  def flat?
@@ -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)
@@ -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,88 @@
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) && RUBY_VERSION >= "2.0.0" && 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, in_source_encoding(node.unescaped, code)]]
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, in_source_encoding(content, code)] unless content.empty?
37
+ when Prism::EmbeddedStatementsNode
38
+ embedded = embedded_source(part, code)
53
39
  exps << [:dynamic, embedded] unless embedded.empty?
54
- end
55
- end
56
- 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
40
+ when Prism::EmbeddedVariableNode
41
+ exps << [:dynamic, in_source_encoding(part.variable.slice, code)]
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}")
83
46
  end
84
47
  end
85
48
  end
86
49
  end
87
50
 
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
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)
102
56
  end
103
57
 
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
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
112
65
 
113
- type, _ = instructions.first
114
- type == :string_literal
66
+ string.dup.force_encoding(code.encoding)
115
67
  end
68
+ end
116
69
 
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
70
+ def on_dynamic(code)
71
+ return [:dynamic, code] if code.include?("\n")
126
72
 
127
- private
73
+ node = RubyExpression.string_literal_node(code)
74
+ return [:dynamic, code] if node.nil?
128
75
 
129
- def on_parse_error(*)
130
- raise ParseError
76
+ temple = [:multi]
77
+ StringSplitter.compile(code, node: node).each do |type, content|
78
+ case type
79
+ when :static
80
+ temple << [:static, content]
81
+ when :dynamic
82
+ temple << on_dynamic(content)
131
83
  end
132
84
  end
133
- else
134
- # Do nothing if ripper is unavailable
135
- def call(ast)
136
- ast
137
- end
85
+ temple
138
86
  end
139
87
  end
140
88
  end
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