liquid 5.4.0 → 5.6.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +11 -0
  3. data/README.md +48 -6
  4. data/lib/liquid/block.rb +8 -4
  5. data/lib/liquid/block_body.rb +28 -10
  6. data/lib/liquid/condition.rb +9 -4
  7. data/lib/liquid/const.rb +8 -0
  8. data/lib/liquid/context.rb +24 -14
  9. data/lib/liquid/deprecations.rb +22 -0
  10. data/lib/liquid/drop.rb +4 -0
  11. data/lib/liquid/environment.rb +159 -0
  12. data/lib/liquid/errors.rb +16 -15
  13. data/lib/liquid/expression.rb +101 -22
  14. data/lib/liquid/forloop_drop.rb +2 -5
  15. data/lib/liquid/lexer.rb +155 -44
  16. data/lib/liquid/locales/en.yml +1 -0
  17. data/lib/liquid/parse_context.rb +29 -6
  18. data/lib/liquid/parse_tree_visitor.rb +1 -1
  19. data/lib/liquid/parser.rb +3 -3
  20. data/lib/liquid/partial_cache.rb +12 -3
  21. data/lib/liquid/range_lookup.rb +14 -4
  22. data/lib/liquid/standardfilters.rb +82 -21
  23. data/lib/liquid/tablerowloop_drop.rb +1 -1
  24. data/lib/liquid/tag/disabler.rb +0 -8
  25. data/lib/liquid/tag.rb +13 -3
  26. data/lib/liquid/tags/assign.rb +1 -3
  27. data/lib/liquid/tags/break.rb +1 -3
  28. data/lib/liquid/tags/capture.rb +0 -2
  29. data/lib/liquid/tags/case.rb +1 -3
  30. data/lib/liquid/tags/comment.rb +60 -3
  31. data/lib/liquid/tags/continue.rb +1 -3
  32. data/lib/liquid/tags/cycle.rb +14 -4
  33. data/lib/liquid/tags/decrement.rb +8 -7
  34. data/lib/liquid/tags/echo.rb +2 -4
  35. data/lib/liquid/tags/for.rb +6 -8
  36. data/lib/liquid/tags/if.rb +3 -5
  37. data/lib/liquid/tags/ifchanged.rb +0 -2
  38. data/lib/liquid/tags/include.rb +8 -8
  39. data/lib/liquid/tags/increment.rb +8 -7
  40. data/lib/liquid/tags/inline_comment.rb +0 -15
  41. data/lib/liquid/tags/raw.rb +2 -4
  42. data/lib/liquid/tags/render.rb +14 -12
  43. data/lib/liquid/tags/table_row.rb +18 -6
  44. data/lib/liquid/tags/unless.rb +3 -5
  45. data/lib/liquid/tags.rb +47 -0
  46. data/lib/liquid/template.rb +60 -57
  47. data/lib/liquid/tokenizer.rb +127 -11
  48. data/lib/liquid/variable.rb +14 -8
  49. data/lib/liquid/variable_lookup.rb +13 -5
  50. data/lib/liquid/version.rb +1 -1
  51. data/lib/liquid.rb +15 -16
  52. metadata +37 -10
  53. data/lib/liquid/strainer_factory.rb +0 -41
@@ -3,41 +3,120 @@
3
3
  module Liquid
4
4
  class Expression
5
5
  LITERALS = {
6
- nil => nil, 'nil' => nil, 'null' => nil, '' => nil,
6
+ nil => nil,
7
+ 'nil' => nil,
8
+ 'null' => nil,
9
+ '' => nil,
7
10
  'true' => true,
8
11
  'false' => false,
9
12
  'blank' => '',
10
- 'empty' => ''
13
+ 'empty' => '',
14
+ # in lax mode, minus sign can be a VariableLookup
15
+ # For simplicity and performace, we treat it like a literal
16
+ '-' => VariableLookup.parse("-", nil).freeze,
11
17
  }.freeze
12
18
 
13
- INTEGERS_REGEX = /\A(-?\d+)\z/
14
- FLOATS_REGEX = /\A(-?\d[\d\.]+)\z/
19
+ DOT = ".".ord
20
+ ZERO = "0".ord
21
+ NINE = "9".ord
22
+ DASH = "-".ord
15
23
 
16
24
  # Use an atomic group (?>...) to avoid pathological backtracing from
17
25
  # malicious input as described in https://github.com/Shopify/liquid/issues/1357
18
- RANGES_REGEX = /\A\(\s*(?>(\S+)\s*\.\.)\s*(\S+)\s*\)\z/
26
+ RANGES_REGEX = /\A\(\s*(?>(\S+)\s*\.\.)\s*(\S+)\s*\)\z/
27
+ INTEGER_REGEX = /\A(-?\d+)\z/
28
+ FLOAT_REGEX = /\A(-?\d+)\.\d+\z/
19
29
 
20
- def self.parse(markup)
21
- return nil unless markup
30
+ class << self
31
+ def parse(markup, ss = StringScanner.new(""), cache = nil)
32
+ return unless markup
22
33
 
23
- markup = markup.strip
24
- if (markup.start_with?('"') && markup.end_with?('"')) ||
25
- (markup.start_with?("'") && markup.end_with?("'"))
26
- return markup[1..-2]
34
+ markup = markup.strip # markup can be a frozen string
35
+
36
+ if (markup.start_with?('"') && markup.end_with?('"')) ||
37
+ (markup.start_with?("'") && markup.end_with?("'"))
38
+ return markup[1..-2]
39
+ elsif LITERALS.key?(markup)
40
+ return LITERALS[markup]
41
+ end
42
+
43
+ # Cache only exists during parsing
44
+ if cache
45
+ return cache[markup] if cache.key?(markup)
46
+
47
+ cache[markup] = inner_parse(markup, ss, cache).freeze
48
+ else
49
+ inner_parse(markup, ss, nil).freeze
50
+ end
27
51
  end
28
52
 
29
- case markup
30
- when INTEGERS_REGEX
31
- Regexp.last_match(1).to_i
32
- when RANGES_REGEX
33
- RangeLookup.parse(Regexp.last_match(1), Regexp.last_match(2))
34
- when FLOATS_REGEX
35
- Regexp.last_match(1).to_f
36
- else
37
- if LITERALS.key?(markup)
38
- LITERALS[markup]
53
+ def inner_parse(markup, ss, cache)
54
+ if (markup.start_with?("(") && markup.end_with?(")")) && markup =~ RANGES_REGEX
55
+ return RangeLookup.parse(
56
+ Regexp.last_match(1),
57
+ Regexp.last_match(2),
58
+ ss,
59
+ cache,
60
+ )
61
+ end
62
+
63
+ if (num = parse_number(markup, ss))
64
+ num
65
+ else
66
+ VariableLookup.parse(markup, ss, cache)
67
+ end
68
+ end
69
+
70
+ def parse_number(markup, ss)
71
+ # check if the markup is simple integer or float
72
+ case markup
73
+ when INTEGER_REGEX
74
+ return Integer(markup, 10)
75
+ when FLOAT_REGEX
76
+ return markup.to_f
77
+ end
78
+
79
+ ss.string = markup
80
+ # the first byte must be a digit or a dash
81
+ byte = ss.scan_byte
82
+
83
+ return false if byte != DASH && (byte < ZERO || byte > NINE)
84
+
85
+ if byte == DASH
86
+ peek_byte = ss.peek_byte
87
+
88
+ # if it starts with a dash, the next byte must be a digit
89
+ return false if peek_byte.nil? || !(peek_byte >= ZERO && peek_byte <= NINE)
90
+ end
91
+
92
+ # The markup could be a float with multiple dots
93
+ first_dot_pos = nil
94
+ num_end_pos = nil
95
+
96
+ while (byte = ss.scan_byte)
97
+ return false if byte != DOT && (byte < ZERO || byte > NINE)
98
+
99
+ # we found our number and now we are just scanning the rest of the string
100
+ next if num_end_pos
101
+
102
+ if byte == DOT
103
+ if first_dot_pos.nil?
104
+ first_dot_pos = ss.pos
105
+ else
106
+ # we found another dot, so we know that the number ends here
107
+ num_end_pos = ss.pos - 1
108
+ end
109
+ end
110
+ end
111
+
112
+ num_end_pos = markup.length if ss.eos?
113
+
114
+ if num_end_pos
115
+ # number ends with a number "123.123"
116
+ markup.byteslice(0, num_end_pos).to_f
39
117
  else
40
- VariableLookup.parse(markup)
118
+ # number ends with a dot "123."
119
+ markup.byteslice(0, first_dot_pos).to_f
41
120
  end
42
121
  end
43
122
  end
@@ -5,7 +5,7 @@ module Liquid
5
5
  # @liquid_type object
6
6
  # @liquid_name forloop
7
7
  # @liquid_summary
8
- # Information about a parent [`for` loop](/api/liquid/tags#for).
8
+ # Information about a parent [`for` loop](/docs/api/liquid/tags/for).
9
9
  class ForloopDrop < Drop
10
10
  def initialize(name, length, parentloop)
11
11
  @name = name
@@ -30,10 +30,7 @@ module Liquid
30
30
  # @liquid_return [forloop]
31
31
  attr_reader :parentloop
32
32
 
33
- def name
34
- Usage.increment('forloop_drop_name')
35
- @name
36
- end
33
+ attr_reader :name
37
34
 
38
35
  # @liquid_public_docs
39
36
  # @liquid_summary
data/lib/liquid/lexer.rb CHANGED
@@ -1,62 +1,173 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "strscan"
4
3
  module Liquid
5
4
  class Lexer
6
- SPECIALS = {
7
- '|' => :pipe,
8
- '.' => :dot,
9
- ':' => :colon,
10
- ',' => :comma,
11
- '[' => :open_square,
12
- ']' => :close_square,
13
- '(' => :open_round,
14
- ')' => :close_round,
15
- '?' => :question,
16
- '-' => :dash,
17
- }.freeze
18
- IDENTIFIER = /[a-zA-Z_][\w-]*\??/
19
- SINGLE_STRING_LITERAL = /'[^\']*'/
5
+ CLOSE_ROUND = [:close_round, ")"].freeze
6
+ CLOSE_SQUARE = [:close_square, "]"].freeze
7
+ COLON = [:colon, ":"].freeze
8
+ COMMA = [:comma, ","].freeze
9
+ COMPARISION_NOT_EQUAL = [:comparison, "!="].freeze
10
+ COMPARISON_CONTAINS = [:comparison, "contains"].freeze
11
+ COMPARISON_EQUAL = [:comparison, "=="].freeze
12
+ COMPARISON_GREATER_THAN = [:comparison, ">"].freeze
13
+ COMPARISON_GREATER_THAN_OR_EQUAL = [:comparison, ">="].freeze
14
+ COMPARISON_LESS_THAN = [:comparison, "<"].freeze
15
+ COMPARISON_LESS_THAN_OR_EQUAL = [:comparison, "<="].freeze
16
+ COMPARISON_NOT_EQUAL_ALT = [:comparison, "<>"].freeze
17
+ DASH = [:dash, "-"].freeze
18
+ DOT = [:dot, "."].freeze
19
+ DOTDOT = [:dotdot, ".."].freeze
20
+ DOT_ORD = ".".ord
20
21
  DOUBLE_STRING_LITERAL = /"[^\"]*"/
22
+ EOS = [:end_of_string].freeze
23
+ IDENTIFIER = /[a-zA-Z_][\w-]*\??/
21
24
  NUMBER_LITERAL = /-?\d+(\.\d+)?/
22
- DOTDOT = /\.\./
23
- COMPARISON_OPERATOR = /==|!=|<>|<=?|>=?|contains(?=\s)/
25
+ OPEN_ROUND = [:open_round, "("].freeze
26
+ OPEN_SQUARE = [:open_square, "["].freeze
27
+ PIPE = [:pipe, "|"].freeze
28
+ QUESTION = [:question, "?"].freeze
29
+ RUBY_WHITESPACE = [" ", "\t", "\r", "\n", "\f"].freeze
30
+ SINGLE_STRING_LITERAL = /'[^\']*'/
24
31
  WHITESPACE_OR_NOTHING = /\s*/
25
32
 
26
- def initialize(input)
27
- @ss = StringScanner.new(input)
33
+ SINGLE_COMPARISON_TOKENS = [].tap do |table|
34
+ table["<".ord] = COMPARISON_LESS_THAN
35
+ table[">".ord] = COMPARISON_GREATER_THAN
36
+ table.freeze
37
+ end
38
+
39
+ TWO_CHARS_COMPARISON_JUMP_TABLE = [].tap do |table|
40
+ table["=".ord] = [].tap do |sub_table|
41
+ sub_table["=".ord] = COMPARISON_EQUAL
42
+ sub_table.freeze
43
+ end
44
+ table["!".ord] = [].tap do |sub_table|
45
+ sub_table["=".ord] = COMPARISION_NOT_EQUAL
46
+ sub_table.freeze
47
+ end
48
+ table.freeze
28
49
  end
29
50
 
30
- def tokenize
31
- @output = []
32
-
33
- until @ss.eos?
34
- @ss.skip(WHITESPACE_OR_NOTHING)
35
- break if @ss.eos?
36
- tok = if (t = @ss.scan(COMPARISON_OPERATOR))
37
- [:comparison, t]
38
- elsif (t = @ss.scan(SINGLE_STRING_LITERAL))
39
- [:string, t]
40
- elsif (t = @ss.scan(DOUBLE_STRING_LITERAL))
41
- [:string, t]
42
- elsif (t = @ss.scan(NUMBER_LITERAL))
43
- [:number, t]
44
- elsif (t = @ss.scan(IDENTIFIER))
45
- [:id, t]
46
- elsif (t = @ss.scan(DOTDOT))
47
- [:dotdot, t]
48
- else
49
- c = @ss.getch
50
- if (s = SPECIALS[c])
51
- [s, c]
51
+ COMPARISON_JUMP_TABLE = [].tap do |table|
52
+ table["<".ord] = [].tap do |sub_table|
53
+ sub_table["=".ord] = COMPARISON_LESS_THAN_OR_EQUAL
54
+ sub_table[">".ord] = COMPARISON_NOT_EQUAL_ALT
55
+ sub_table.freeze
56
+ end
57
+ table[">".ord] = [].tap do |sub_table|
58
+ sub_table["=".ord] = COMPARISON_GREATER_THAN_OR_EQUAL
59
+ sub_table.freeze
60
+ end
61
+ table.freeze
62
+ end
63
+
64
+ NEXT_MATCHER_JUMP_TABLE = [].tap do |table|
65
+ "a".upto("z") do |c|
66
+ table[c.ord] = [:id, IDENTIFIER].freeze
67
+ table[c.upcase.ord] = [:id, IDENTIFIER].freeze
68
+ end
69
+ table["_".ord] = [:id, IDENTIFIER].freeze
70
+
71
+ "0".upto("9") do |c|
72
+ table[c.ord] = [:number, NUMBER_LITERAL].freeze
73
+ end
74
+ table["-".ord] = [:number, NUMBER_LITERAL].freeze
75
+
76
+ table["'".ord] = [:string, SINGLE_STRING_LITERAL].freeze
77
+ table["\"".ord] = [:string, DOUBLE_STRING_LITERAL].freeze
78
+ table.freeze
79
+ end
80
+
81
+ SPECIAL_TABLE = [].tap do |table|
82
+ table["|".ord] = PIPE
83
+ table[".".ord] = DOT
84
+ table[":".ord] = COLON
85
+ table[",".ord] = COMMA
86
+ table["[".ord] = OPEN_SQUARE
87
+ table["]".ord] = CLOSE_SQUARE
88
+ table["(".ord] = OPEN_ROUND
89
+ table[")".ord] = CLOSE_ROUND
90
+ table["?".ord] = QUESTION
91
+ table["-".ord] = DASH
92
+ end
93
+
94
+ NUMBER_TABLE = [].tap do |table|
95
+ "0".upto("9") do |c|
96
+ table[c.ord] = true
97
+ end
98
+ table.freeze
99
+ end
100
+
101
+ # rubocop:disable Metrics/BlockNesting
102
+ class << self
103
+ def tokenize(ss)
104
+ output = []
105
+
106
+ until ss.eos?
107
+ ss.skip(WHITESPACE_OR_NOTHING)
108
+
109
+ break if ss.eos?
110
+
111
+ start_pos = ss.pos
112
+ peeked = ss.peek_byte
113
+
114
+ if (special = SPECIAL_TABLE[peeked])
115
+ ss.scan_byte
116
+ # Special case for ".."
117
+ if special == DOT && ss.peek_byte == DOT_ORD
118
+ ss.scan_byte
119
+ output << DOTDOT
120
+ elsif special == DASH
121
+ # Special case for negative numbers
122
+ if (peeked_byte = ss.peek_byte) && NUMBER_TABLE[peeked_byte]
123
+ ss.pos -= 1
124
+ output << [:number, ss.scan(NUMBER_LITERAL)]
125
+ else
126
+ output << special
127
+ end
128
+ else
129
+ output << special
130
+ end
131
+ elsif (sub_table = TWO_CHARS_COMPARISON_JUMP_TABLE[peeked])
132
+ ss.scan_byte
133
+ if (peeked_byte = ss.peek_byte) && (found = sub_table[peeked_byte])
134
+ output << found
135
+ ss.scan_byte
136
+ else
137
+ raise_syntax_error(start_pos, ss)
138
+ end
139
+ elsif (sub_table = COMPARISON_JUMP_TABLE[peeked])
140
+ ss.scan_byte
141
+ if (peeked_byte = ss.peek_byte) && (found = sub_table[peeked_byte])
142
+ output << found
143
+ ss.scan_byte
144
+ else
145
+ output << SINGLE_COMPARISON_TOKENS[peeked]
146
+ end
52
147
  else
53
- raise SyntaxError, "Unexpected character #{c}"
148
+ type, pattern = NEXT_MATCHER_JUMP_TABLE[peeked]
149
+
150
+ if type && (t = ss.scan(pattern))
151
+ # Special case for "contains"
152
+ output << if type == :id && t == "contains" && output.last&.first != :dot
153
+ COMPARISON_CONTAINS
154
+ else
155
+ [type, t]
156
+ end
157
+ else
158
+ raise_syntax_error(start_pos, ss)
159
+ end
54
160
  end
55
161
  end
56
- @output << tok
162
+ # rubocop:enable Metrics/BlockNesting
163
+ output << EOS
57
164
  end
58
165
 
59
- @output << [:end_of_string]
166
+ def raise_syntax_error(start_pos, ss)
167
+ ss.pos = start_pos
168
+ # the character could be a UTF-8 character, use getch to get all the bytes
169
+ raise SyntaxError, "Unexpected character #{ss.getch}"
170
+ end
60
171
  end
61
172
  end
62
173
  end
@@ -15,6 +15,7 @@
15
15
  include: "Error in tag 'include' - Valid syntax: include '[template]' (with|for) [object|collection]"
16
16
  inline_comment_invalid: "Syntax error in tag '#' - Each line of comments must be prefixed by the '#' character"
17
17
  invalid_delimiter: "'%{tag}' is not a valid delimiter for %{block_name} tags. use %{block_delimiter}"
18
+ invalid_template_encoding: "Invalid template encoding"
18
19
  render: "Syntax error in tag 'render' - Template name must be a quoted string"
19
20
  table_row: "Syntax Error in 'table_row loop' - Valid syntax: table_row [item] in [collection] cols=3"
20
21
  tag_never_closed: "'%{block_name}' tag was never closed"
@@ -3,14 +3,27 @@
3
3
  module Liquid
4
4
  class ParseContext
5
5
  attr_accessor :locale, :line_number, :trim_whitespace, :depth
6
- attr_reader :partial, :warnings, :error_mode
6
+ attr_reader :partial, :warnings, :error_mode, :environment
7
7
 
8
- def initialize(options = {})
8
+ def initialize(options = Const::EMPTY_HASH)
9
+ @environment = options.fetch(:environment, Environment.default)
9
10
  @template_options = options ? options.dup : {}
10
11
 
11
12
  @locale = @template_options[:locale] ||= I18n.new
12
13
  @warnings = []
13
14
 
15
+ # constructing new StringScanner in Lexer, Tokenizer, etc is expensive
16
+ # This StringScanner will be shared by all of them
17
+ @string_scanner = StringScanner.new("")
18
+
19
+ @expression_cache = if options[:expression_cache].nil?
20
+ {}
21
+ elsif options[:expression_cache].respond_to?(:[]) && options[:expression_cache].respond_to?(:[]=)
22
+ options[:expression_cache]
23
+ elsif options[:expression_cache]
24
+ {}
25
+ end
26
+
14
27
  self.depth = 0
15
28
  self.partial = false
16
29
  end
@@ -23,19 +36,29 @@ module Liquid
23
36
  Liquid::BlockBody.new
24
37
  end
25
38
 
26
- def new_tokenizer(markup, start_line_number: nil, for_liquid_tag: false)
27
- Tokenizer.new(markup, line_number: start_line_number, for_liquid_tag: for_liquid_tag)
39
+ def new_parser(input)
40
+ @string_scanner.string = input
41
+ Parser.new(@string_scanner)
42
+ end
43
+
44
+ def new_tokenizer(source, start_line_number: nil, for_liquid_tag: false)
45
+ Tokenizer.new(
46
+ source: source,
47
+ string_scanner: @string_scanner,
48
+ line_number: start_line_number,
49
+ for_liquid_tag: for_liquid_tag,
50
+ )
28
51
  end
29
52
 
30
53
  def parse_expression(markup)
31
- Expression.parse(markup)
54
+ Expression.parse(markup, @string_scanner, @expression_cache)
32
55
  end
33
56
 
34
57
  def partial=(value)
35
58
  @partial = value
36
59
  @options = value ? partial_options : @template_options
37
60
 
38
- @error_mode = @options[:error_mode] || Template.error_mode
61
+ @error_mode = @options[:error_mode] || @environment.error_mode
39
62
  end
40
63
 
41
64
  def partial_options
@@ -36,7 +36,7 @@ module Liquid
36
36
  protected
37
37
 
38
38
  def children
39
- @node.respond_to?(:nodelist) ? Array(@node.nodelist) : []
39
+ @node.respond_to?(:nodelist) ? Array(@node.nodelist) : Const::EMPTY_ARRAY
40
40
  end
41
41
  end
42
42
  end
data/lib/liquid/parser.rb CHANGED
@@ -3,8 +3,8 @@
3
3
  module Liquid
4
4
  class Parser
5
5
  def initialize(input)
6
- l = Lexer.new(input)
7
- @tokens = l.tokenize
6
+ ss = input.is_a?(StringScanner) ? input : StringScanner.new(input)
7
+ @tokens = Lexer.tokenize(ss)
8
8
  @p = 0 # pointer to current location
9
9
  end
10
10
 
@@ -53,7 +53,7 @@ module Liquid
53
53
  str = consume
54
54
  str << variable_lookups
55
55
  when :open_square
56
- str = consume
56
+ str = consume.dup
57
57
  str << expression
58
58
  str << consume(:close_square)
59
59
  str << variable_lookups
@@ -4,7 +4,8 @@ module Liquid
4
4
  class PartialCache
5
5
  def self.load(template_name, context:, parse_context:)
6
6
  cached_partials = context.registers[:cached_partials]
7
- cached = cached_partials[template_name]
7
+ cache_key = "#{template_name}:#{parse_context.error_mode}"
8
+ cached = cached_partials[cache_key]
8
9
  return cached if cached
9
10
 
10
11
  file_system = context.registers[:file_system]
@@ -15,8 +16,16 @@ module Liquid
15
16
  template_factory = context.registers[:template_factory]
16
17
  template = template_factory.for(template_name)
17
18
 
18
- partial = template.parse(source, parse_context)
19
- cached_partials[template_name] = partial
19
+ begin
20
+ partial = template.parse(source, parse_context)
21
+ rescue Liquid::Error => e
22
+ e.template_name = template&.name || template_name
23
+ raise e
24
+ end
25
+
26
+ partial.name ||= template_name
27
+
28
+ cached_partials[cache_key] = partial
20
29
  ensure
21
30
  parse_context.partial = false
22
31
  end
@@ -2,13 +2,23 @@
2
2
 
3
3
  module Liquid
4
4
  class RangeLookup
5
- def self.parse(start_markup, end_markup)
6
- start_obj = Expression.parse(start_markup)
7
- end_obj = Expression.parse(end_markup)
5
+ def self.parse(start_markup, end_markup, string_scanner, cache = nil)
6
+ start_obj = Expression.parse(start_markup, string_scanner, cache)
7
+ end_obj = Expression.parse(end_markup, string_scanner, cache)
8
8
  if start_obj.respond_to?(:evaluate) || end_obj.respond_to?(:evaluate)
9
9
  new(start_obj, end_obj)
10
10
  else
11
- start_obj.to_i..end_obj.to_i
11
+ begin
12
+ start_obj.to_i..end_obj.to_i
13
+ rescue NoMethodError
14
+ invalid_expr = start_markup unless start_obj.respond_to?(:to_i)
15
+ invalid_expr ||= end_markup unless end_obj.respond_to?(:to_i)
16
+ if invalid_expr
17
+ raise Liquid::SyntaxError, "Invalid expression type '#{invalid_expr}' in range expression"
18
+ end
19
+
20
+ raise
21
+ end
12
22
  end
13
23
  end
14
24