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 +4 -4
- data/README.md +1 -1
- data/lib/liquid/context.rb +8 -1
- data/lib/liquid/expression.rb +92 -21
- data/lib/liquid/lexer.rb +63 -127
- data/lib/liquid/parse_context.rb +25 -3
- data/lib/liquid/parser.rb +2 -2
- data/lib/liquid/range_lookup.rb +3 -3
- data/lib/liquid/tags/cycle.rb +7 -1
- data/lib/liquid/tags/for.rb +1 -1
- data/lib/liquid/tags/if.rb +1 -1
- data/lib/liquid/tokenizer.rb +123 -13
- data/lib/liquid/variable.rb +1 -1
- data/lib/liquid/variable_lookup.rb +13 -5
- data/lib/liquid/version.rb +1 -1
- data/lib/liquid.rb +4 -1
- metadata +5 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f56c8d1a7b6c038267d7e838aae2d8dba5c99e5fbb563d1163966cbbc9b60673
|
4
|
+
data.tar.gz: c26e7a9288ee25b325ff6f2db4d21b8ed4f9ad97eb64f1aebeefd971e5c5bc46
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
|
data/lib/liquid/context.rb
CHANGED
@@ -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)
|
data/lib/liquid/expression.rb
CHANGED
@@ -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
|
-
|
17
|
-
|
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
|
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
|
-
|
24
|
-
|
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
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
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
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
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
|
-
|
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
|
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
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
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
|
-
|
129
|
+
output << special
|
189
130
|
end
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
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
|
-
[
|
145
|
+
output << SINGLE_COMPARISON_TOKENS[peeked]
|
218
146
|
end
|
219
147
|
else
|
220
|
-
|
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
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
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
|
data/lib/liquid/parse_context.rb
CHANGED
@@ -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
|
28
|
-
|
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
data/lib/liquid/range_lookup.rb
CHANGED
@@ -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
|
data/lib/liquid/tags/cycle.rb
CHANGED
@@ -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
|
-
|
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
|
|
data/lib/liquid/tags/for.rb
CHANGED
data/lib/liquid/tags/if.rb
CHANGED
data/lib/liquid/tokenizer.rb
CHANGED
@@ -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
|
-
|
8
|
-
|
9
|
-
|
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
|
-
@
|
12
|
-
@
|
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
|
-
|
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
|
-
|
54
|
+
if @for_liquid_tag
|
55
|
+
@tokens = @source.split("\n")
|
56
|
+
else
|
57
|
+
@tokens << shift_normal until @ss.eos?
|
58
|
+
end
|
32
59
|
|
33
|
-
|
60
|
+
@source = nil
|
61
|
+
@ss = nil
|
62
|
+
end
|
34
63
|
|
35
|
-
|
64
|
+
def shift_normal
|
65
|
+
token = next_token
|
36
66
|
|
37
|
-
|
38
|
-
|
39
|
-
|
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
|
-
|
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
|
data/lib/liquid/variable.rb
CHANGED
@@ -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(
|
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(
|
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
|
data/lib/liquid/version.rb
CHANGED
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.
|
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:
|
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:
|
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:
|
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.
|
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: []
|