liquid 5.6.0 → 5.6.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d1a39a98605d86cc2cf586a59d1a402ed708ad3722061083514eeeea602249d9
4
- data.tar.gz: d3de02a25cada366198f08959e8d6565b120b961978810b98dda3170acee7b7e
3
+ metadata.gz: f56c8d1a7b6c038267d7e838aae2d8dba5c99e5fbb563d1163966cbbc9b60673
4
+ data.tar.gz: c26e7a9288ee25b325ff6f2db4d21b8ed4f9ad97eb64f1aebeefd971e5c5bc46
5
5
  SHA512:
6
- metadata.gz: fa084464c4927940f8edc1c0206858bdf4c39ca5bd1b632d2786ab6b11b235f5a5696dad9c9e9bdf79640d897539baaa5c56044230ed6901d1c34f930c0e3f0a
7
- data.tar.gz: dbe05e17ceb7c73461ed0b4f838a03af7853ec47a454ada51c632763adb31e28cf8e4fd3d89c2ed0c5c2f8d91eea2e3b661c8257ab266d7895c398ef1ac047f5
6
+ metadata.gz: a81801d7e2976bd5825e19349a410c5800e85c30d31502da013cab9b95fff5cdeb7debcd9e7ccedee4a3056456ed3433a3c8751258f89c9a24c8b46a223e2f19
7
+ data.tar.gz: b81704d1039698ce2e0391ad2bfbb6d69daba677dfbc1d4180bb7b6a4018981ed7984af44b64de84d8c745561cacf86999a1f326d1045296d696c5b62edd8849
data/README.md CHANGED
@@ -91,7 +91,7 @@ Liquid::Template.parse(<<~LIQUID, environment: email_environment)
91
91
  LIQUID
92
92
  ```
93
93
 
94
- By using Environments, you ensure that custom tags and filters are only available in the contexts where they are needed, making your Liquid templates more robust and easier to manage.
94
+ By using Environments, you ensure that custom tags and filters are only available in the contexts where they are needed, making your Liquid templates more robust and easier to manage. For smaller projects, a global environment is available via `Liquid::Environment.default`.
95
95
 
96
96
  ### Error Modes
97
97
 
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "lru_redux"
4
+
3
5
  module Liquid
4
6
  # Context keeps the variable stack and resolves variables, as well as keywords
5
7
  #
@@ -39,6 +41,11 @@ module Liquid
39
41
  @filters = []
40
42
  @global_filter = nil
41
43
  @disabled_tags = {}
44
+ @expression_cache = LruRedux::ThreadSafeCache.new(1000)
45
+
46
+ # Instead of constructing new StringScanner objects for each Expression parse,
47
+ # we recycle the same one.
48
+ @string_scanner = StringScanner.new("")
42
49
 
43
50
  @registers.static[:cached_partials] ||= {}
44
51
  @registers.static[:file_system] ||= environment.file_system
@@ -176,7 +183,7 @@ module Liquid
176
183
  # Example:
177
184
  # products == empty #=> products.empty?
178
185
  def [](expression)
179
- evaluate(Expression.parse(expression))
186
+ evaluate(Expression.parse(expression, @string_scanner, @expression_cache))
180
187
  end
181
188
 
182
189
  def key?(key)
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "lru_redux"
4
+
3
5
  module Liquid
4
6
  class Expression
5
7
  LITERALS = {
@@ -10,37 +12,106 @@ module Liquid
10
12
  'true' => true,
11
13
  'false' => false,
12
14
  'blank' => '',
13
- 'empty' => ''
15
+ 'empty' => '',
16
+ # in lax mode, minus sign can be a VariableLookup
17
+ # For simplicity and performace, we treat it like a literal
18
+ '-' => VariableLookup.parse("-", nil).freeze,
14
19
  }.freeze
15
20
 
16
- INTEGERS_REGEX = /\A(-?\d+)\z/
17
- FLOATS_REGEX = /\A(-?\d[\d\.]+)\z/
21
+ DOT = ".".ord
22
+ ZERO = "0".ord
23
+ NINE = "9".ord
24
+ DASH = "-".ord
18
25
 
19
26
  # Use an atomic group (?>...) to avoid pathological backtracing from
20
27
  # malicious input as described in https://github.com/Shopify/liquid/issues/1357
21
- RANGES_REGEX = /\A\(\s*(?>(\S+)\s*\.\.)\s*(\S+)\s*\)\z/
28
+ RANGES_REGEX = /\A\(\s*(?>(\S+)\s*\.\.)\s*(\S+)\s*\)\z/
29
+ INTEGER_REGEX = /\A(-?\d+)\z/
30
+ FLOAT_REGEX = /\A(-?\d+)\.\d+\z/
31
+
32
+ class << self
33
+ def parse(markup, ss = StringScanner.new(""), cache = nil)
34
+ return unless markup
35
+
36
+ markup = markup.strip # markup can be a frozen string
22
37
 
23
- def self.parse(markup)
24
- return nil unless markup
38
+ if (markup.start_with?('"') && markup.end_with?('"')) ||
39
+ (markup.start_with?("'") && markup.end_with?("'"))
40
+ return markup[1..-2]
41
+ elsif LITERALS.key?(markup)
42
+ return LITERALS[markup]
43
+ end
44
+
45
+ # Cache only exists during parsing
46
+ if cache
47
+ return cache[markup] if cache.key?(markup)
25
48
 
26
- markup = markup.strip
27
- if (markup.start_with?('"') && markup.end_with?('"')) ||
28
- (markup.start_with?("'") && markup.end_with?("'"))
29
- return markup[1..-2]
49
+ cache[markup] = inner_parse(markup, ss, cache).freeze
50
+ else
51
+ inner_parse(markup, ss, nil).freeze
52
+ end
30
53
  end
31
54
 
32
- case markup
33
- when INTEGERS_REGEX
34
- Regexp.last_match(1).to_i
35
- when RANGES_REGEX
36
- RangeLookup.parse(Regexp.last_match(1), Regexp.last_match(2))
37
- when FLOATS_REGEX
38
- Regexp.last_match(1).to_f
39
- else
40
- if LITERALS.key?(markup)
41
- LITERALS[markup]
55
+ def inner_parse(markup, ss, cache)
56
+ if (markup.start_with?("(") && markup.end_with?(")")) && markup =~ RANGES_REGEX
57
+ return RangeLookup.parse(
58
+ Regexp.last_match(1),
59
+ Regexp.last_match(2),
60
+ ss,
61
+ cache,
62
+ )
63
+ end
64
+
65
+ if (num = parse_number(markup, ss))
66
+ num
67
+ else
68
+ VariableLookup.parse(markup, ss, cache)
69
+ end
70
+ end
71
+
72
+ def parse_number(markup, ss)
73
+ # check if the markup is simple integer or float
74
+ case markup
75
+ when INTEGER_REGEX
76
+ return Integer(markup, 10)
77
+ when FLOAT_REGEX
78
+ return markup.to_f
79
+ end
80
+
81
+ ss.string = markup
82
+ # the first byte must be a digit, a period, or a dash
83
+ byte = ss.scan_byte
84
+
85
+ return false if byte != DASH && byte != DOT && (byte < ZERO || byte > NINE)
86
+
87
+ # The markup could be a float with multiple dots
88
+ first_dot_pos = nil
89
+ num_end_pos = nil
90
+
91
+ while (byte = ss.scan_byte)
92
+ return false if byte != DOT && (byte < ZERO || byte > NINE)
93
+
94
+ # we found our number and now we are just scanning the rest of the string
95
+ next if num_end_pos
96
+
97
+ if byte == DOT
98
+ if first_dot_pos.nil?
99
+ first_dot_pos = ss.pos
100
+ else
101
+ # we found another dot, so we know that the number ends here
102
+ num_end_pos = ss.pos - 1
103
+ end
104
+ end
105
+ end
106
+
107
+ num_end_pos = markup.length if ss.eos?
108
+
109
+ if num_end_pos
110
+ # number ends with a number "123.123"
111
+ markup.byteslice(0, num_end_pos).to_f
42
112
  else
43
- VariableLookup.parse(markup)
113
+ # number ends with a dot "123."
114
+ markup.byteslice(0, first_dot_pos).to_f
44
115
  end
45
116
  end
46
117
  end
data/lib/liquid/lexer.rb CHANGED
@@ -1,66 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "strscan"
4
-
5
3
  module Liquid
6
- class Lexer1
7
- SPECIALS = {
8
- '|' => :pipe,
9
- '.' => :dot,
10
- ':' => :colon,
11
- ',' => :comma,
12
- '[' => :open_square,
13
- ']' => :close_square,
14
- '(' => :open_round,
15
- ')' => :close_round,
16
- '?' => :question,
17
- '-' => :dash,
18
- }.freeze
19
- IDENTIFIER = /[a-zA-Z_][\w-]*\??/
20
- SINGLE_STRING_LITERAL = /'[^\']*'/
21
- DOUBLE_STRING_LITERAL = /"[^\"]*"/
22
- STRING_LITERAL = Regexp.union(SINGLE_STRING_LITERAL, DOUBLE_STRING_LITERAL)
23
- NUMBER_LITERAL = /-?\d+(\.\d+)?/
24
- DOTDOT = /\.\./
25
- COMPARISON_OPERATOR = /==|!=|<>|<=?|>=?|contains(?=\s)/
26
- WHITESPACE_OR_NOTHING = /\s*/
27
-
28
- def initialize(input)
29
- @ss = StringScanner.new(input)
30
- end
31
-
32
- def tokenize
33
- @output = []
34
-
35
- until @ss.eos?
36
- @ss.skip(WHITESPACE_OR_NOTHING)
37
- break if @ss.eos?
38
- tok = if (t = @ss.scan(COMPARISON_OPERATOR))
39
- [:comparison, t]
40
- elsif (t = @ss.scan(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]
52
- else
53
- raise SyntaxError, "Unexpected character #{c}"
54
- end
55
- end
56
- @output << tok
57
- end
58
-
59
- @output << [:end_of_string]
60
- end
61
- end
62
-
63
- class Lexer2
4
+ class Lexer
64
5
  CLOSE_ROUND = [:close_round, ")"].freeze
65
6
  CLOSE_SQUARE = [:close_square, "]"].freeze
66
7
  COLON = [:colon, ":"].freeze
@@ -92,6 +33,7 @@ module Liquid
92
33
  SINGLE_COMPARISON_TOKENS = [].tap do |table|
93
34
  table["<".ord] = COMPARISON_LESS_THAN
94
35
  table[">".ord] = COMPARISON_GREATER_THAN
36
+ table.freeze
95
37
  end
96
38
 
97
39
  TWO_CHARS_COMPARISON_JUMP_TABLE = [].tap do |table|
@@ -103,18 +45,17 @@ module Liquid
103
45
  sub_table["=".ord] = COMPARISION_NOT_EQUAL
104
46
  sub_table.freeze
105
47
  end
48
+ table.freeze
106
49
  end
107
50
 
108
51
  COMPARISON_JUMP_TABLE = [].tap do |table|
109
52
  table["<".ord] = [].tap do |sub_table|
110
53
  sub_table["=".ord] = COMPARISON_LESS_THAN_OR_EQUAL
111
54
  sub_table[">".ord] = COMPARISON_NOT_EQUAL_ALT
112
- RUBY_WHITESPACE.each { |c| sub_table[c.ord] = COMPARISON_LESS_THAN }
113
55
  sub_table.freeze
114
56
  end
115
57
  table[">".ord] = [].tap do |sub_table|
116
58
  sub_table["=".ord] = COMPARISON_GREATER_THAN_OR_EQUAL
117
- RUBY_WHITESPACE.each { |c| sub_table[c.ord] = COMPARISON_GREATER_THAN }
118
59
  sub_table.freeze
119
60
  end
120
61
  table.freeze
@@ -157,81 +98,76 @@ module Liquid
157
98
  table.freeze
158
99
  end
159
100
 
160
- def initialize(input)
161
- @ss = StringScanner.new(input)
162
- end
163
-
164
101
  # rubocop:disable Metrics/BlockNesting
165
- def tokenize
166
- @output = []
167
-
168
- until @ss.eos?
169
- @ss.skip(WHITESPACE_OR_NOTHING)
170
-
171
- break if @ss.eos?
172
-
173
- start_pos = @ss.pos
174
- peeked = @ss.peek_byte
175
-
176
- if (special = SPECIAL_TABLE[peeked])
177
- @ss.scan_byte
178
- # Special case for ".."
179
- if special == DOT && @ss.peek_byte == DOT_ORD
180
- @ss.scan_byte
181
- @output << DOTDOT
182
- elsif special == DASH
183
- # Special case for negative numbers
184
- if (peeked_byte = @ss.peek_byte) && NUMBER_TABLE[peeked_byte]
185
- @ss.pos -= 1
186
- @output << [:number, @ss.scan(NUMBER_LITERAL)]
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
187
128
  else
188
- @output << special
129
+ output << special
189
130
  end
190
- else
191
- @output << special
192
- end
193
- elsif (sub_table = TWO_CHARS_COMPARISON_JUMP_TABLE[peeked])
194
- @ss.scan_byte
195
- if (peeked_byte = @ss.peek_byte) && (found = sub_table[peeked_byte])
196
- @output << found
197
- @ss.scan_byte
198
- else
199
- raise_syntax_error(start_pos)
200
- end
201
- elsif (sub_table = COMPARISON_JUMP_TABLE[peeked])
202
- @ss.scan_byte
203
- if (peeked_byte = @ss.peek_byte) && (found = sub_table[peeked_byte])
204
- @output << found
205
- @ss.scan_byte
206
- else
207
- @output << SINGLE_COMPARISON_TOKENS[peeked]
208
- end
209
- else
210
- type, pattern = NEXT_MATCHER_JUMP_TABLE[peeked]
211
-
212
- if type && (t = @ss.scan(pattern))
213
- # Special case for "contains"
214
- @output << if type == :id && t == "contains" && @output.last&.first != :dot
215
- COMPARISON_CONTAINS
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
216
144
  else
217
- [type, t]
145
+ output << SINGLE_COMPARISON_TOKENS[peeked]
218
146
  end
219
147
  else
220
- raise_syntax_error(start_pos)
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
221
160
  end
222
161
  end
162
+ # rubocop:enable Metrics/BlockNesting
163
+ output << EOS
223
164
  end
224
- # rubocop:enable Metrics/BlockNesting
225
165
 
226
- @output << EOS
227
- end
228
-
229
- def raise_syntax_error(start_pos)
230
- @ss.pos = start_pos
231
- # the character could be a UTF-8 character, use getch to get all the bytes
232
- raise SyntaxError, "Unexpected character #{@ss.getch}"
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
233
171
  end
234
172
  end
235
-
236
- Lexer = StringScanner.instance_methods.include?(:scan_byte) ? Lexer2 : Lexer1
237
173
  end
@@ -12,6 +12,18 @@ module Liquid
12
12
  @locale = @template_options[:locale] ||= I18n.new
13
13
  @warnings = []
14
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
+
15
27
  self.depth = 0
16
28
  self.partial = false
17
29
  end
@@ -24,12 +36,22 @@ module Liquid
24
36
  Liquid::BlockBody.new
25
37
  end
26
38
 
27
- def new_tokenizer(markup, start_line_number: nil, for_liquid_tag: false)
28
- 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
+ )
29
51
  end
30
52
 
31
53
  def parse_expression(markup)
32
- Expression.parse(markup)
54
+ Expression.parse(markup, @string_scanner, @expression_cache)
33
55
  end
34
56
 
35
57
  def partial=(value)
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
 
@@ -2,9 +2,9 @@
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
@@ -68,7 +68,13 @@ module Liquid
68
68
  def variables_from_string(markup)
69
69
  markup.split(',').collect do |var|
70
70
  var =~ /\s*(#{QuotedFragment})\s*/o
71
- Regexp.last_match(1) ? parse_expression(Regexp.last_match(1)) : nil
71
+ next unless Regexp.last_match(1)
72
+
73
+ # Expression Parser returns cached objects, and we need to dup them to
74
+ # start the cycle over for each new cycle call.
75
+ # Liquid-C does not have a cache, so we don't need to dup the object.
76
+ var = parse_expression(Regexp.last_match(1))
77
+ var.is_a?(VariableLookup) ? var.dup : var
72
78
  end.compact
73
79
  end
74
80
 
@@ -88,7 +88,7 @@ module Liquid
88
88
  end
89
89
 
90
90
  def strict_parse(markup)
91
- p = Parser.new(markup)
91
+ p = @parse_context.new_parser(markup)
92
92
  @variable_name = p.consume(:id)
93
93
  raise SyntaxError, options[:locale].t("errors.syntax.for_invalid_in") unless p.id?('in')
94
94
 
@@ -102,7 +102,7 @@ module Liquid
102
102
  end
103
103
 
104
104
  def strict_parse(markup)
105
- p = Parser.new(markup)
105
+ p = @parse_context.new_parser(markup)
106
106
  condition = parse_binary_comparisons(p)
107
107
  p.consume(:end_of_string)
108
108
  condition
@@ -1,20 +1,43 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "strscan"
4
+
3
5
  module Liquid
4
6
  class Tokenizer
5
7
  attr_reader :line_number, :for_liquid_tag
6
8
 
7
- def initialize(source, line_numbers = false, line_number: nil, for_liquid_tag: false)
8
- @source = source.to_s.to_str
9
- @line_number = line_number || (line_numbers ? 1 : nil)
9
+ TAG_END = /%\}/
10
+ TAG_OR_VARIABLE_START = /\{[\{\%]/
11
+ NEWLINE = /\n/
12
+
13
+ OPEN_CURLEY = "{".ord
14
+ CLOSE_CURLEY = "}".ord
15
+ PERCENTAGE = "%".ord
16
+
17
+ def initialize(
18
+ source:,
19
+ string_scanner:,
20
+ line_numbers: false,
21
+ line_number: nil,
22
+ for_liquid_tag: false
23
+ )
24
+ @line_number = line_number || (line_numbers ? 1 : nil)
10
25
  @for_liquid_tag = for_liquid_tag
11
- @offset = 0
12
- @tokens = tokenize
26
+ @source = source.to_s.to_str
27
+ @offset = 0
28
+ @tokens = []
29
+
30
+ if @source
31
+ @ss = string_scanner
32
+ @ss.string = @source
33
+ tokenize
34
+ end
13
35
  end
14
36
 
15
37
  def shift
16
38
  token = @tokens[@offset]
17
- return nil unless token
39
+
40
+ return unless token
18
41
 
19
42
  @offset += 1
20
43
 
@@ -28,18 +51,105 @@ module Liquid
28
51
  private
29
52
 
30
53
  def tokenize
31
- return [] if @source.empty?
54
+ if @for_liquid_tag
55
+ @tokens = @source.split("\n")
56
+ else
57
+ @tokens << shift_normal until @ss.eos?
58
+ end
32
59
 
33
- return @source.split("\n") if @for_liquid_tag
60
+ @source = nil
61
+ @ss = nil
62
+ end
34
63
 
35
- tokens = @source.split(TemplateParser)
64
+ def shift_normal
65
+ token = next_token
36
66
 
37
- # removes the rogue empty element at the beginning of the array
38
- if tokens[0]&.empty?
39
- @offset += 1
67
+ return unless token
68
+
69
+ token
70
+ end
71
+
72
+ def next_token
73
+ # possible states: :text, :tag, :variable
74
+ byte_a = @ss.peek_byte
75
+
76
+ if byte_a == OPEN_CURLEY
77
+ @ss.scan_byte
78
+
79
+ byte_b = @ss.peek_byte
80
+
81
+ if byte_b == PERCENTAGE
82
+ @ss.scan_byte
83
+ return next_tag_token
84
+ elsif byte_b == OPEN_CURLEY
85
+ @ss.scan_byte
86
+ return next_variable_token
87
+ end
88
+
89
+ @ss.pos -= 1
40
90
  end
41
91
 
42
- tokens
92
+ next_text_token
93
+ end
94
+
95
+ def next_text_token
96
+ start = @ss.pos
97
+
98
+ unless @ss.skip_until(TAG_OR_VARIABLE_START)
99
+ token = @ss.rest
100
+ @ss.terminate
101
+ return token
102
+ end
103
+
104
+ pos = @ss.pos -= 2
105
+ @source.byteslice(start, pos - start)
106
+ end
107
+
108
+ def next_variable_token
109
+ start = @ss.pos - 2
110
+
111
+ byte_a = byte_b = @ss.scan_byte
112
+
113
+ while byte_b
114
+ byte_a = @ss.scan_byte while byte_a && (byte_a != CLOSE_CURLEY && byte_a != OPEN_CURLEY)
115
+
116
+ break unless byte_a
117
+
118
+ if @ss.eos?
119
+ return byte_a == CLOSE_CURLEY ? @source.byteslice(start, @ss.pos - start) : "{{"
120
+ end
121
+
122
+ byte_b = @ss.scan_byte
123
+
124
+ if byte_a == CLOSE_CURLEY
125
+ if byte_b == CLOSE_CURLEY
126
+ return @source.byteslice(start, @ss.pos - start)
127
+ elsif byte_b != CLOSE_CURLEY
128
+ @ss.pos -= 1
129
+ return @source.byteslice(start, @ss.pos - start)
130
+ end
131
+ elsif byte_a == OPEN_CURLEY && byte_b == PERCENTAGE
132
+ return next_tag_token_with_start(start)
133
+ end
134
+
135
+ byte_a = byte_b
136
+ end
137
+
138
+ "{{"
139
+ end
140
+
141
+ def next_tag_token
142
+ start = @ss.pos - 2
143
+ if (len = @ss.skip_until(TAG_END))
144
+ @source.byteslice(start, len + 2)
145
+ else
146
+ "{%"
147
+ end
148
+ end
149
+
150
+ def next_tag_token_with_start(start)
151
+ @ss.skip_until(TAG_END)
152
+ @source.byteslice(start, @ss.pos - start)
43
153
  end
44
154
  end
45
155
  end
@@ -61,7 +61,7 @@ module Liquid
61
61
 
62
62
  def strict_parse(markup)
63
63
  @filters = []
64
- p = Parser.new(markup)
64
+ p = @parse_context.new_parser(markup)
65
65
 
66
66
  return if p.look(:end_of_string)
67
67
 
@@ -6,16 +6,20 @@ module Liquid
6
6
 
7
7
  attr_reader :name, :lookups
8
8
 
9
- def self.parse(markup)
10
- new(markup)
9
+ def self.parse(markup, string_scanner, cache = nil)
10
+ new(markup, string_scanner, cache)
11
11
  end
12
12
 
13
- def initialize(markup)
13
+ def initialize(markup, string_scanner = StringScanner.new(""), cache = nil)
14
14
  lookups = markup.scan(VariableParser)
15
15
 
16
16
  name = lookups.shift
17
17
  if name&.start_with?('[') && name&.end_with?(']')
18
- name = Expression.parse(name[1..-2])
18
+ name = Expression.parse(
19
+ name[1..-2],
20
+ string_scanner,
21
+ cache,
22
+ )
19
23
  end
20
24
  @name = name
21
25
 
@@ -25,7 +29,11 @@ module Liquid
25
29
  @lookups.each_index do |i|
26
30
  lookup = lookups[i]
27
31
  if lookup&.start_with?('[') && lookup&.end_with?(']')
28
- lookups[i] = Expression.parse(lookup[1..-2])
32
+ lookups[i] = Expression.parse(
33
+ lookup[1..-2],
34
+ string_scanner,
35
+ cache,
36
+ )
29
37
  elsif COMMAND_METHODS.include?(lookup)
30
38
  @command_flags |= 1 << i
31
39
  end
@@ -2,5 +2,5 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module Liquid
5
- VERSION = "5.6.0"
5
+ VERSION = "5.6.1"
6
6
  end
data/lib/liquid.rb CHANGED
@@ -21,6 +21,8 @@
21
21
  # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
22
  # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
23
 
24
+ require "strscan"
25
+
24
26
  module Liquid
25
27
  FilterSeparator = /\|/
26
28
  ArgumentSeparator = ','
@@ -44,6 +46,7 @@ module Liquid
44
46
  VariableParser = /\[(?>[^\[\]]+|\g<0>)*\]|#{VariableSegment}+\??/o
45
47
 
46
48
  RAISE_EXCEPTION_LAMBDA = ->(_e) { raise }
49
+ HAS_STRING_SCANNER_SCAN_BYTE = StringScanner.instance_methods.include?(:scan_byte)
47
50
  end
48
51
 
49
52
  require "liquid/version"
@@ -68,7 +71,6 @@ require 'liquid/extensions'
68
71
  require 'liquid/errors'
69
72
  require 'liquid/interrupts'
70
73
  require 'liquid/strainer_template'
71
- require 'liquid/expression'
72
74
  require 'liquid/context'
73
75
  require 'liquid/tag'
74
76
  require 'liquid/block_body'
@@ -77,6 +79,7 @@ require 'liquid/variable'
77
79
  require 'liquid/variable_lookup'
78
80
  require 'liquid/range_lookup'
79
81
  require 'liquid/resource_limits'
82
+ require 'liquid/expression'
80
83
  require 'liquid/template'
81
84
  require 'liquid/condition'
82
85
  require 'liquid/utils'
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: liquid
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.6.0
4
+ version: 5.6.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tobias Lütke
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2024-12-19 00:00:00.000000000 Z
10
+ date: 2025-01-13 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: strscan
@@ -15,14 +15,14 @@ dependencies:
15
15
  requirements:
16
16
  - - ">="
17
17
  - !ruby/object:Gem::Version
18
- version: '0'
18
+ version: 3.1.1
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - ">="
24
24
  - !ruby/object:Gem::Version
25
- version: '0'
25
+ version: 3.1.1
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: bigdecimal
28
28
  requirement: !ruby/object:Gem::Requirement
@@ -158,7 +158,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
158
158
  - !ruby/object:Gem::Version
159
159
  version: 1.3.7
160
160
  requirements: []
161
- rubygems_version: 3.6.1
161
+ rubygems_version: 3.6.2
162
162
  specification_version: 4
163
163
  summary: A secure, non-evaling end user template engine with aesthetic markup.
164
164
  test_files: []