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.
- checksums.yaml +4 -4
- data/.github/workflows/test.yml +1 -5
- data/CHANGELOG.md +64 -0
- data/Gemfile +6 -14
- data/README.md +1 -1
- data/REFERENCE.md +1 -1
- data/bin/console +3 -4
- data/bin/stackprof +2 -2
- data/haml.gemspec +6 -3
- data/lib/haml/attribute_builder.rb +25 -14
- data/lib/haml/attribute_compiler.rb +24 -12
- data/lib/haml/attribute_parser.rb +53 -79
- data/lib/haml/compiler/children_compiler.rb +1 -3
- data/lib/haml/compiler/script_compiler.rb +12 -14
- data/lib/haml/compiler/tag_compiler.rb +8 -5
- data/lib/haml/dynamic_merger.rb +1 -1
- data/lib/haml/engine.rb +3 -3
- data/lib/haml/filters/plain.rb +6 -1
- data/lib/haml/helpers.rb +1 -1
- data/lib/haml/parser.rb +48 -30
- data/lib/haml/preserver.rb +52 -0
- data/lib/haml/rails_helpers.rb +4 -12
- data/lib/haml/ruby_expression.rb +25 -18
- data/lib/haml/string_splitter.rb +61 -113
- data/lib/haml/template.rb +1 -1
- data/lib/haml/temple_line_counter.rb +15 -1
- data/lib/haml/util.rb +13 -10
- data/lib/haml/version.rb +1 -1
- data/lib/haml.rb +1 -5
- metadata +21 -17
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}"
|
|
56
|
+
"#{after_preamble.tr("\n", ';')}#{@precompiled}"
|
|
57
57
|
end
|
|
58
58
|
end
|
|
59
59
|
end
|
data/lib/haml/filters/plain.rb
CHANGED
|
@@ -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.
|
|
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
data/lib/haml/parser.rb
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require '
|
|
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? &&
|
|
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(''
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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 =
|
|
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
|
-
#
|
|
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? &&
|
|
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
|
|
690
|
-
#
|
|
691
|
-
|
|
692
|
-
|
|
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 = "{"
|
|
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(''
|
|
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] == ?: &&
|
|
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? &&
|
|
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
|
|
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]
|
|
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
|
|
863
|
+
# Unlike #balance, this balances lexed tokens to balance something like `{ a: "}" }` correctly.
|
|
849
864
|
def balance_tokens(buf, start, finish, count: 0)
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
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
|
-
|
|
871
|
+
elsif finish.include?(type)
|
|
857
872
|
count -= 1
|
|
858
873
|
end
|
|
859
874
|
|
|
860
875
|
if count == 0
|
|
861
|
-
|
|
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
|
-
|
|
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
|
data/lib/haml/rails_helpers.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
# frozen_string_literal:
|
|
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 =
|
|
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
|
-
|
|
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)
|
data/lib/haml/ruby_expression.rb
CHANGED
|
@@ -1,32 +1,39 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
-
require '
|
|
2
|
+
require 'prism'
|
|
3
3
|
|
|
4
4
|
module Haml
|
|
5
|
-
class RubyExpression
|
|
6
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
29
|
-
|
|
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
|
data/lib/haml/string_splitter.rb
CHANGED
|
@@ -1,140 +1,88 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
42
|
-
|
|
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
|
-
|
|
45
|
-
_, type, str = tokens.shift
|
|
28
|
+
private
|
|
46
29
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
when
|
|
52
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
return
|
|
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
|
-
|
|
114
|
-
type == :string_literal
|
|
66
|
+
string.dup.force_encoding(code.encoding)
|
|
115
67
|
end
|
|
68
|
+
end
|
|
116
69
|
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
73
|
+
node = RubyExpression.string_literal_node(code)
|
|
74
|
+
return [:dynamic, code] if node.nil?
|
|
128
75
|
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
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
|
@@ -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
|