liquid 5.4.0 → 5.6.4

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.
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