liquid 4.0.0 → 5.10.0
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 +5 -5
- data/History.md +235 -2
- data/README.md +58 -8
- data/lib/liquid/block.rb +51 -20
- data/lib/liquid/block_body.rb +216 -82
- data/lib/liquid/condition.rb +83 -32
- data/lib/liquid/const.rb +8 -0
- data/lib/liquid/context.rb +130 -59
- data/lib/liquid/deprecations.rb +22 -0
- data/lib/liquid/document.rb +47 -9
- data/lib/liquid/drop.rb +8 -2
- data/lib/liquid/environment.rb +159 -0
- data/lib/liquid/errors.rb +23 -20
- data/lib/liquid/expression.rb +114 -31
- data/lib/liquid/extensions.rb +8 -0
- data/lib/liquid/file_system.rb +6 -4
- data/lib/liquid/forloop_drop.rb +51 -4
- data/lib/liquid/i18n.rb +5 -3
- data/lib/liquid/interrupts.rb +3 -1
- data/lib/liquid/lexer.rb +165 -39
- data/lib/liquid/locales/en.yml +16 -6
- data/lib/liquid/parse_context.rb +62 -7
- data/lib/liquid/parse_tree_visitor.rb +42 -0
- data/lib/liquid/parser.rb +31 -19
- data/lib/liquid/parser_switching.rb +42 -3
- data/lib/liquid/partial_cache.rb +33 -0
- data/lib/liquid/profiler/hooks.rb +26 -14
- data/lib/liquid/profiler.rb +67 -86
- data/lib/liquid/range_lookup.rb +26 -6
- data/lib/liquid/registers.rb +51 -0
- data/lib/liquid/resource_limits.rb +47 -8
- data/lib/liquid/snippet_drop.rb +22 -0
- data/lib/liquid/standardfilters.rb +813 -137
- data/lib/liquid/strainer_template.rb +62 -0
- data/lib/liquid/tablerowloop_drop.rb +64 -5
- data/lib/liquid/tag/disableable.rb +22 -0
- data/lib/liquid/tag/disabler.rb +13 -0
- data/lib/liquid/tag.rb +42 -6
- data/lib/liquid/tags/assign.rb +46 -18
- data/lib/liquid/tags/break.rb +15 -4
- data/lib/liquid/tags/capture.rb +26 -18
- data/lib/liquid/tags/case.rb +108 -32
- data/lib/liquid/tags/comment.rb +76 -4
- data/lib/liquid/tags/continue.rb +15 -13
- data/lib/liquid/tags/cycle.rb +117 -34
- data/lib/liquid/tags/decrement.rb +30 -23
- data/lib/liquid/tags/doc.rb +81 -0
- data/lib/liquid/tags/echo.rb +39 -0
- data/lib/liquid/tags/for.rb +109 -96
- data/lib/liquid/tags/if.rb +72 -41
- data/lib/liquid/tags/ifchanged.rb +10 -11
- data/lib/liquid/tags/include.rb +89 -63
- data/lib/liquid/tags/increment.rb +31 -20
- data/lib/liquid/tags/inline_comment.rb +28 -0
- data/lib/liquid/tags/raw.rb +25 -13
- data/lib/liquid/tags/render.rb +151 -0
- data/lib/liquid/tags/snippet.rb +45 -0
- data/lib/liquid/tags/table_row.rb +104 -21
- data/lib/liquid/tags/unless.rb +37 -20
- data/lib/liquid/tags.rb +51 -0
- data/lib/liquid/template.rb +90 -106
- data/lib/liquid/template_factory.rb +9 -0
- data/lib/liquid/tokenizer.rb +143 -13
- data/lib/liquid/usage.rb +8 -0
- data/lib/liquid/utils.rb +114 -5
- data/lib/liquid/variable.rb +119 -45
- data/lib/liquid/variable_lookup.rb +35 -13
- data/lib/liquid/version.rb +3 -1
- data/lib/liquid.rb +31 -18
- metadata +56 -107
- data/lib/liquid/strainer.rb +0 -66
- data/test/fixtures/en_locale.yml +0 -9
- data/test/integration/assign_test.rb +0 -48
- data/test/integration/blank_test.rb +0 -106
- data/test/integration/capture_test.rb +0 -50
- data/test/integration/context_test.rb +0 -32
- data/test/integration/document_test.rb +0 -19
- data/test/integration/drop_test.rb +0 -273
- data/test/integration/error_handling_test.rb +0 -260
- data/test/integration/filter_test.rb +0 -178
- data/test/integration/hash_ordering_test.rb +0 -23
- data/test/integration/output_test.rb +0 -123
- data/test/integration/parsing_quirks_test.rb +0 -118
- data/test/integration/render_profiling_test.rb +0 -154
- data/test/integration/security_test.rb +0 -66
- data/test/integration/standard_filter_test.rb +0 -535
- data/test/integration/tags/break_tag_test.rb +0 -15
- data/test/integration/tags/continue_tag_test.rb +0 -15
- data/test/integration/tags/for_tag_test.rb +0 -410
- data/test/integration/tags/if_else_tag_test.rb +0 -188
- data/test/integration/tags/include_tag_test.rb +0 -238
- data/test/integration/tags/increment_tag_test.rb +0 -23
- data/test/integration/tags/raw_tag_test.rb +0 -31
- data/test/integration/tags/standard_tag_test.rb +0 -296
- data/test/integration/tags/statements_test.rb +0 -111
- data/test/integration/tags/table_row_test.rb +0 -64
- data/test/integration/tags/unless_else_tag_test.rb +0 -26
- data/test/integration/template_test.rb +0 -323
- data/test/integration/trim_mode_test.rb +0 -525
- data/test/integration/variable_test.rb +0 -92
- data/test/test_helper.rb +0 -117
- data/test/unit/block_unit_test.rb +0 -58
- data/test/unit/condition_unit_test.rb +0 -158
- data/test/unit/context_unit_test.rb +0 -483
- data/test/unit/file_system_unit_test.rb +0 -35
- data/test/unit/i18n_unit_test.rb +0 -37
- data/test/unit/lexer_unit_test.rb +0 -51
- data/test/unit/parser_unit_test.rb +0 -82
- data/test/unit/regexp_unit_test.rb +0 -44
- data/test/unit/strainer_unit_test.rb +0 -148
- data/test/unit/tag_unit_test.rb +0 -21
- data/test/unit/tags/case_tag_unit_test.rb +0 -10
- data/test/unit/tags/for_tag_unit_test.rb +0 -13
- data/test/unit/tags/if_tag_unit_test.rb +0 -8
- data/test/unit/template_unit_test.rb +0 -78
- data/test/unit/tokenizer_unit_test.rb +0 -55
- data/test/unit/variable_unit_test.rb +0 -162
data/lib/liquid/tokenizer.rb
CHANGED
|
@@ -1,31 +1,161 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "strscan"
|
|
4
|
+
|
|
1
5
|
module Liquid
|
|
2
6
|
class Tokenizer
|
|
3
|
-
attr_reader :line_number
|
|
7
|
+
attr_reader :line_number, :for_liquid_tag
|
|
8
|
+
|
|
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)
|
|
25
|
+
@for_liquid_tag = for_liquid_tag
|
|
26
|
+
@source = source.to_s.to_str
|
|
27
|
+
@offset = 0
|
|
28
|
+
@tokens = []
|
|
4
29
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
30
|
+
if @source
|
|
31
|
+
@ss = string_scanner
|
|
32
|
+
@ss.string = @source
|
|
33
|
+
tokenize
|
|
34
|
+
end
|
|
9
35
|
end
|
|
10
36
|
|
|
11
37
|
def shift
|
|
12
|
-
token = @tokens
|
|
13
|
-
|
|
38
|
+
token = @tokens[@offset]
|
|
39
|
+
|
|
40
|
+
return unless token
|
|
41
|
+
|
|
42
|
+
@offset += 1
|
|
43
|
+
|
|
44
|
+
if @line_number
|
|
45
|
+
@line_number += @for_liquid_tag ? 1 : token.count("\n")
|
|
46
|
+
end
|
|
47
|
+
|
|
14
48
|
token
|
|
15
49
|
end
|
|
16
50
|
|
|
17
51
|
private
|
|
18
52
|
|
|
19
53
|
def tokenize
|
|
20
|
-
|
|
21
|
-
|
|
54
|
+
if @for_liquid_tag
|
|
55
|
+
@tokens = @source.split("\n")
|
|
56
|
+
else
|
|
57
|
+
@tokens << shift_normal until @ss.eos?
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
@source = nil
|
|
61
|
+
@ss = nil
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def shift_normal
|
|
65
|
+
token = next_token
|
|
66
|
+
|
|
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
|
|
22
80
|
|
|
23
|
-
|
|
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
|
|
24
88
|
|
|
25
|
-
|
|
26
|
-
|
|
89
|
+
@ss.pos -= 1
|
|
90
|
+
end
|
|
91
|
+
|
|
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
|
+
rescue ::ArgumentError => e
|
|
107
|
+
if e.message == "invalid byte sequence in #{@ss.string.encoding}"
|
|
108
|
+
raise SyntaxError, "Invalid byte sequence in #{@ss.string.encoding}"
|
|
109
|
+
else
|
|
110
|
+
raise
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def next_variable_token
|
|
115
|
+
start = @ss.pos - 2
|
|
116
|
+
|
|
117
|
+
byte_a = byte_b = @ss.scan_byte
|
|
118
|
+
|
|
119
|
+
while byte_b
|
|
120
|
+
byte_a = @ss.scan_byte while byte_a && (byte_a != CLOSE_CURLEY && byte_a != OPEN_CURLEY)
|
|
121
|
+
|
|
122
|
+
break unless byte_a
|
|
123
|
+
|
|
124
|
+
if @ss.eos?
|
|
125
|
+
return byte_a == CLOSE_CURLEY ? @source.byteslice(start, @ss.pos - start) : "{{"
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
byte_b = @ss.scan_byte
|
|
129
|
+
|
|
130
|
+
if byte_a == CLOSE_CURLEY
|
|
131
|
+
if byte_b == CLOSE_CURLEY
|
|
132
|
+
return @source.byteslice(start, @ss.pos - start)
|
|
133
|
+
elsif byte_b != CLOSE_CURLEY
|
|
134
|
+
@ss.pos -= 1
|
|
135
|
+
return @source.byteslice(start, @ss.pos - start)
|
|
136
|
+
end
|
|
137
|
+
elsif byte_a == OPEN_CURLEY && byte_b == PERCENTAGE
|
|
138
|
+
return next_tag_token_with_start(start)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
byte_a = byte_b
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
"{{"
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def next_tag_token
|
|
148
|
+
start = @ss.pos - 2
|
|
149
|
+
if (len = @ss.skip_until(TAG_END))
|
|
150
|
+
@source.byteslice(start, len + 2)
|
|
151
|
+
else
|
|
152
|
+
"{%"
|
|
153
|
+
end
|
|
154
|
+
end
|
|
27
155
|
|
|
28
|
-
|
|
156
|
+
def next_tag_token_with_start(start)
|
|
157
|
+
@ss.skip_until(TAG_END)
|
|
158
|
+
@source.byteslice(start, @ss.pos - start)
|
|
29
159
|
end
|
|
30
160
|
end
|
|
31
161
|
end
|
data/lib/liquid/usage.rb
ADDED
data/lib/liquid/utils.rb
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Liquid
|
|
2
4
|
module Utils
|
|
5
|
+
DECIMAL_REGEX = /\A-?\d+\.\d+\z/
|
|
6
|
+
UNIX_TIMESTAMP_REGEX = /\A\d+\z/
|
|
7
|
+
|
|
3
8
|
def self.slice_collection(collection, from, to)
|
|
4
9
|
if (from != 0 || !to.nil?) && collection.respond_to?(:load_slice)
|
|
5
10
|
collection.load_slice(from, to)
|
|
@@ -10,7 +15,7 @@ module Liquid
|
|
|
10
15
|
|
|
11
16
|
def self.slice_collection_using_each(collection, from, to)
|
|
12
17
|
segments = []
|
|
13
|
-
index
|
|
18
|
+
index = 0
|
|
14
19
|
|
|
15
20
|
# Maintains Ruby 1.8.7 String#each behaviour on 1.9
|
|
16
21
|
if collection.is_a?(String)
|
|
@@ -46,11 +51,11 @@ module Liquid
|
|
|
46
51
|
def self.to_number(obj)
|
|
47
52
|
case obj
|
|
48
53
|
when Float
|
|
49
|
-
BigDecimal
|
|
54
|
+
BigDecimal(obj.to_s)
|
|
50
55
|
when Numeric
|
|
51
56
|
obj
|
|
52
57
|
when String
|
|
53
|
-
(obj.strip
|
|
58
|
+
DECIMAL_REGEX.match?(obj.strip) ? BigDecimal(obj) : obj.to_i
|
|
54
59
|
else
|
|
55
60
|
if obj.respond_to?(:to_number)
|
|
56
61
|
obj.to_number
|
|
@@ -69,9 +74,9 @@ module Liquid
|
|
|
69
74
|
end
|
|
70
75
|
|
|
71
76
|
case obj
|
|
72
|
-
when 'now'
|
|
77
|
+
when 'now', 'today'
|
|
73
78
|
Time.now
|
|
74
|
-
when
|
|
79
|
+
when UNIX_TIMESTAMP_REGEX, Integer
|
|
75
80
|
Time.at(obj.to_i)
|
|
76
81
|
when String
|
|
77
82
|
Time.parse(obj)
|
|
@@ -79,5 +84,109 @@ module Liquid
|
|
|
79
84
|
rescue ::ArgumentError
|
|
80
85
|
nil
|
|
81
86
|
end
|
|
87
|
+
|
|
88
|
+
def self.to_liquid_value(obj)
|
|
89
|
+
# Enable "obj" to represent itself as a primitive value like integer, string, or boolean
|
|
90
|
+
return obj.to_liquid_value if obj.respond_to?(:to_liquid_value)
|
|
91
|
+
|
|
92
|
+
# Otherwise return the object itself
|
|
93
|
+
obj
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def self.to_s(obj, seen = {})
|
|
97
|
+
case obj
|
|
98
|
+
when Hash
|
|
99
|
+
# If the custom hash implementation overrides `#to_s`, use their
|
|
100
|
+
# custom implementation. Otherwise we use Liquid's default
|
|
101
|
+
# implementation.
|
|
102
|
+
if obj.class.instance_method(:to_s) == HASH_TO_S_METHOD
|
|
103
|
+
hash_inspect(obj, seen)
|
|
104
|
+
else
|
|
105
|
+
obj.to_s
|
|
106
|
+
end
|
|
107
|
+
when Array
|
|
108
|
+
array_inspect(obj, seen)
|
|
109
|
+
else
|
|
110
|
+
obj.to_s
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def self.inspect(obj, seen = {})
|
|
115
|
+
case obj
|
|
116
|
+
when Hash
|
|
117
|
+
# If the custom hash implementation overrides `#inspect`, use their
|
|
118
|
+
# custom implementation. Otherwise we use Liquid's default
|
|
119
|
+
# implementation.
|
|
120
|
+
if obj.class.instance_method(:inspect) == HASH_INSPECT_METHOD
|
|
121
|
+
hash_inspect(obj, seen)
|
|
122
|
+
else
|
|
123
|
+
obj.inspect
|
|
124
|
+
end
|
|
125
|
+
when Array
|
|
126
|
+
array_inspect(obj, seen)
|
|
127
|
+
else
|
|
128
|
+
obj.inspect
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def self.array_inspect(arr, seen = {})
|
|
133
|
+
if seen[arr.object_id]
|
|
134
|
+
return "[...]"
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
seen[arr.object_id] = true
|
|
138
|
+
str = +"["
|
|
139
|
+
cursor = 0
|
|
140
|
+
len = arr.length
|
|
141
|
+
|
|
142
|
+
while cursor < len
|
|
143
|
+
if cursor > 0
|
|
144
|
+
str << ", "
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
item_str = inspect(arr[cursor], seen)
|
|
148
|
+
str << item_str
|
|
149
|
+
cursor += 1
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
str << "]"
|
|
153
|
+
str
|
|
154
|
+
ensure
|
|
155
|
+
seen.delete(arr.object_id)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def self.hash_inspect(hash, seen = {})
|
|
159
|
+
if seen[hash.object_id]
|
|
160
|
+
return "{...}"
|
|
161
|
+
end
|
|
162
|
+
seen[hash.object_id] = true
|
|
163
|
+
|
|
164
|
+
str = +"{"
|
|
165
|
+
first = true
|
|
166
|
+
hash.each do |key, value|
|
|
167
|
+
if first
|
|
168
|
+
first = false
|
|
169
|
+
else
|
|
170
|
+
str << ", "
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
key_str = inspect(key, seen)
|
|
174
|
+
str << key_str
|
|
175
|
+
str << "=>"
|
|
176
|
+
|
|
177
|
+
value_str = inspect(value, seen)
|
|
178
|
+
str << value_str
|
|
179
|
+
end
|
|
180
|
+
str << "}"
|
|
181
|
+
str
|
|
182
|
+
ensure
|
|
183
|
+
seen.delete(hash.object_id)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
HASH_TO_S_METHOD = Hash.instance_method(:to_s)
|
|
187
|
+
private_constant :HASH_TO_S_METHOD
|
|
188
|
+
|
|
189
|
+
HASH_INSPECT_METHOD = Hash.instance_method(:inspect)
|
|
190
|
+
private_constant :HASH_INSPECT_METHOD
|
|
82
191
|
end
|
|
83
192
|
end
|
data/lib/liquid/variable.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Liquid
|
|
2
4
|
# Holds variables. Variables are only loaded "just in time"
|
|
3
5
|
# and are not evaluated as part of the render stage
|
|
@@ -10,19 +12,25 @@ module Liquid
|
|
|
10
12
|
# {{ user | link }}
|
|
11
13
|
#
|
|
12
14
|
class Variable
|
|
13
|
-
|
|
15
|
+
FilterMarkupRegex = /#{FilterSeparator}\s*(.*)/om
|
|
16
|
+
FilterParser = /(?:\s+|#{QuotedFragment}|#{ArgumentSeparator})+/o
|
|
17
|
+
FilterArgsRegex = /(?:#{FilterArgumentSeparator}|#{ArgumentSeparator})\s*((?:\w+\s*\:\s*)?#{QuotedFragment})/o
|
|
18
|
+
JustTagAttributes = /\A#{TagAttributes}\z/o
|
|
19
|
+
MarkupWithQuotedFragment = /(#{QuotedFragment})(.*)/om
|
|
20
|
+
|
|
14
21
|
attr_accessor :filters, :name, :line_number
|
|
15
22
|
attr_reader :parse_context
|
|
16
23
|
alias_method :options, :parse_context
|
|
24
|
+
|
|
17
25
|
include ParserSwitching
|
|
18
26
|
|
|
19
27
|
def initialize(markup, parse_context)
|
|
20
|
-
@markup
|
|
21
|
-
@name
|
|
28
|
+
@markup = markup
|
|
29
|
+
@name = nil
|
|
22
30
|
@parse_context = parse_context
|
|
23
|
-
@line_number
|
|
31
|
+
@line_number = parse_context.line_number
|
|
24
32
|
|
|
25
|
-
|
|
33
|
+
strict_parse_with_error_mode_fallback(markup)
|
|
26
34
|
end
|
|
27
35
|
|
|
28
36
|
def raw
|
|
@@ -35,35 +43,48 @@ module Liquid
|
|
|
35
43
|
|
|
36
44
|
def lax_parse(markup)
|
|
37
45
|
@filters = []
|
|
38
|
-
return unless markup =~
|
|
46
|
+
return unless markup =~ MarkupWithQuotedFragment
|
|
39
47
|
|
|
40
|
-
name_markup
|
|
41
|
-
filter_markup =
|
|
42
|
-
@name
|
|
43
|
-
if filter_markup =~
|
|
44
|
-
filters =
|
|
48
|
+
name_markup = Regexp.last_match(1)
|
|
49
|
+
filter_markup = Regexp.last_match(2)
|
|
50
|
+
@name = parse_context.parse_expression(name_markup)
|
|
51
|
+
if filter_markup =~ FilterMarkupRegex
|
|
52
|
+
filters = Regexp.last_match(1).scan(FilterParser)
|
|
45
53
|
filters.each do |f|
|
|
46
54
|
next unless f =~ /\w+/
|
|
47
55
|
filtername = Regexp.last_match(0)
|
|
48
|
-
filterargs = f.scan(
|
|
49
|
-
@filters <<
|
|
56
|
+
filterargs = f.scan(FilterArgsRegex).flatten
|
|
57
|
+
@filters << lax_parse_filter_expressions(filtername, filterargs)
|
|
50
58
|
end
|
|
51
59
|
end
|
|
52
60
|
end
|
|
53
61
|
|
|
54
62
|
def strict_parse(markup)
|
|
55
63
|
@filters = []
|
|
56
|
-
p =
|
|
64
|
+
p = @parse_context.new_parser(markup)
|
|
57
65
|
|
|
58
|
-
|
|
66
|
+
return if p.look(:end_of_string)
|
|
67
|
+
|
|
68
|
+
@name = parse_context.safe_parse_expression(p)
|
|
59
69
|
while p.consume?(:pipe)
|
|
60
70
|
filtername = p.consume(:id)
|
|
61
|
-
filterargs = p.consume?(:colon) ? parse_filterargs(p) :
|
|
62
|
-
@filters <<
|
|
71
|
+
filterargs = p.consume?(:colon) ? parse_filterargs(p) : Const::EMPTY_ARRAY
|
|
72
|
+
@filters << lax_parse_filter_expressions(filtername, filterargs)
|
|
63
73
|
end
|
|
64
74
|
p.consume(:end_of_string)
|
|
65
75
|
end
|
|
66
76
|
|
|
77
|
+
def rigid_parse(markup)
|
|
78
|
+
@filters = []
|
|
79
|
+
p = @parse_context.new_parser(markup)
|
|
80
|
+
|
|
81
|
+
return if p.look(:end_of_string)
|
|
82
|
+
|
|
83
|
+
@name = parse_context.safe_parse_expression(p)
|
|
84
|
+
@filters << rigid_parse_filter_expressions(p) while p.consume?(:pipe)
|
|
85
|
+
p.consume(:end_of_string)
|
|
86
|
+
end
|
|
87
|
+
|
|
67
88
|
def parse_filterargs(p)
|
|
68
89
|
# first argument
|
|
69
90
|
filterargs = [p.argument]
|
|
@@ -73,37 +94,103 @@ module Liquid
|
|
|
73
94
|
end
|
|
74
95
|
|
|
75
96
|
def render(context)
|
|
76
|
-
obj =
|
|
97
|
+
obj = context.evaluate(@name)
|
|
98
|
+
|
|
99
|
+
@filters.each do |filter_name, filter_args, filter_kwargs|
|
|
77
100
|
filter_args = evaluate_filter_expressions(context, filter_args, filter_kwargs)
|
|
78
|
-
context.invoke(filter_name,
|
|
101
|
+
obj = context.invoke(filter_name, obj, *filter_args)
|
|
79
102
|
end
|
|
80
103
|
|
|
81
|
-
|
|
104
|
+
context.apply_global_filter(obj)
|
|
105
|
+
end
|
|
82
106
|
|
|
83
|
-
|
|
107
|
+
def render_to_output_buffer(context, output)
|
|
108
|
+
obj = render(context)
|
|
109
|
+
render_obj_to_output(obj, output)
|
|
110
|
+
output
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def render_obj_to_output(obj, output)
|
|
114
|
+
case obj
|
|
115
|
+
when NilClass
|
|
116
|
+
# Do nothing
|
|
117
|
+
when Array
|
|
118
|
+
obj.each do |o|
|
|
119
|
+
render_obj_to_output(o, output)
|
|
120
|
+
end
|
|
121
|
+
else
|
|
122
|
+
output << Liquid::Utils.to_s(obj)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def disabled?(_context)
|
|
127
|
+
false
|
|
128
|
+
end
|
|
84
129
|
|
|
85
|
-
|
|
130
|
+
def disabled_tags
|
|
131
|
+
[]
|
|
86
132
|
end
|
|
87
133
|
|
|
88
134
|
private
|
|
89
135
|
|
|
90
|
-
def
|
|
91
|
-
filter_args
|
|
92
|
-
keyword_args =
|
|
136
|
+
def lax_parse_filter_expressions(filter_name, unparsed_args)
|
|
137
|
+
filter_args = []
|
|
138
|
+
keyword_args = nil
|
|
93
139
|
unparsed_args.each do |a|
|
|
94
|
-
if matches = a.match(
|
|
95
|
-
keyword_args
|
|
140
|
+
if (matches = a.match(JustTagAttributes))
|
|
141
|
+
keyword_args ||= {}
|
|
142
|
+
keyword_args[matches[1]] = parse_context.parse_expression(matches[2])
|
|
96
143
|
else
|
|
97
|
-
filter_args <<
|
|
144
|
+
filter_args << parse_context.parse_expression(a)
|
|
98
145
|
end
|
|
99
146
|
end
|
|
100
147
|
result = [filter_name, filter_args]
|
|
148
|
+
result << keyword_args if keyword_args
|
|
149
|
+
result
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Surprisingly, positional and keyword arguments can be mixed.
|
|
153
|
+
#
|
|
154
|
+
# filter = filtername [":" filterargs?]
|
|
155
|
+
# filterargs = argument ("," argument)*
|
|
156
|
+
# argument = (positional_argument | keyword_argument)
|
|
157
|
+
# positional_argument = expression
|
|
158
|
+
# keyword_argument = id ":" expression
|
|
159
|
+
def rigid_parse_filter_expressions(p)
|
|
160
|
+
filtername = p.consume(:id)
|
|
161
|
+
filter_args = []
|
|
162
|
+
keyword_args = {}
|
|
163
|
+
|
|
164
|
+
if p.consume?(:colon)
|
|
165
|
+
# Parse first argument (no leading comma)
|
|
166
|
+
argument(p, filter_args, keyword_args) unless end_of_arguments?(p)
|
|
167
|
+
|
|
168
|
+
# Parse remaining arguments (with leading commas) and optional trailing comma
|
|
169
|
+
argument(p, filter_args, keyword_args) while p.consume?(:comma) && !end_of_arguments?(p)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
result = [filtername, filter_args]
|
|
101
173
|
result << keyword_args unless keyword_args.empty?
|
|
102
174
|
result
|
|
103
175
|
end
|
|
104
176
|
|
|
177
|
+
def argument(p, positional_arguments, keyword_arguments)
|
|
178
|
+
if p.look(:id) && p.look(:colon, 1)
|
|
179
|
+
key = p.consume(:id)
|
|
180
|
+
p.consume(:colon)
|
|
181
|
+
value = parse_context.safe_parse_expression(p)
|
|
182
|
+
keyword_arguments[key] = value
|
|
183
|
+
else
|
|
184
|
+
positional_arguments << parse_context.safe_parse_expression(p)
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def end_of_arguments?(p)
|
|
189
|
+
p.look(:pipe) || p.look(:end_of_string)
|
|
190
|
+
end
|
|
191
|
+
|
|
105
192
|
def evaluate_filter_expressions(context, filter_args, filter_kwargs)
|
|
106
|
-
parsed_args = filter_args.map{ |expr| context.evaluate(expr) }
|
|
193
|
+
parsed_args = filter_args.map { |expr| context.evaluate(expr) }
|
|
107
194
|
if filter_kwargs
|
|
108
195
|
parsed_kwargs = {}
|
|
109
196
|
filter_kwargs.each do |key, expr|
|
|
@@ -114,22 +201,9 @@ module Liquid
|
|
|
114
201
|
parsed_args
|
|
115
202
|
end
|
|
116
203
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
@markup =~ QuotedFragment
|
|
122
|
-
name = Regexp.last_match(0)
|
|
123
|
-
|
|
124
|
-
error = TaintedError.new("variable '#{name}' is tainted and was not escaped")
|
|
125
|
-
error.line_number = line_number
|
|
126
|
-
error.template_name = context.template_name
|
|
127
|
-
|
|
128
|
-
case Template.taint_mode
|
|
129
|
-
when :warn
|
|
130
|
-
context.warnings << error
|
|
131
|
-
when :error
|
|
132
|
-
raise error
|
|
204
|
+
class ParseTreeVisitor < Liquid::ParseTreeVisitor
|
|
205
|
+
def children
|
|
206
|
+
[@node.name] + @node.filters.flatten
|
|
133
207
|
end
|
|
134
208
|
end
|
|
135
209
|
end
|
|
@@ -1,43 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Liquid
|
|
2
4
|
class VariableLookup
|
|
3
|
-
|
|
4
|
-
COMMAND_METHODS = ['size'.freeze, 'first'.freeze, 'last'.freeze]
|
|
5
|
+
COMMAND_METHODS = ['size', 'first', 'last'].freeze
|
|
5
6
|
|
|
6
7
|
attr_reader :name, :lookups
|
|
7
8
|
|
|
8
|
-
def self.parse(markup)
|
|
9
|
-
new(markup)
|
|
9
|
+
def self.parse(markup, string_scanner = StringScanner.new(""), cache = nil)
|
|
10
|
+
new(markup, string_scanner, cache)
|
|
10
11
|
end
|
|
11
12
|
|
|
12
|
-
def initialize(markup)
|
|
13
|
+
def initialize(markup, string_scanner = StringScanner.new(""), cache = nil)
|
|
13
14
|
lookups = markup.scan(VariableParser)
|
|
14
15
|
|
|
15
16
|
name = lookups.shift
|
|
16
|
-
if name
|
|
17
|
-
name = Expression.parse(
|
|
17
|
+
if name&.start_with?('[') && name&.end_with?(']')
|
|
18
|
+
name = Expression.parse(
|
|
19
|
+
name[1..-2],
|
|
20
|
+
string_scanner,
|
|
21
|
+
cache,
|
|
22
|
+
)
|
|
18
23
|
end
|
|
19
24
|
@name = name
|
|
20
25
|
|
|
21
|
-
@lookups
|
|
26
|
+
@lookups = lookups
|
|
22
27
|
@command_flags = 0
|
|
23
28
|
|
|
24
29
|
@lookups.each_index do |i|
|
|
25
30
|
lookup = lookups[i]
|
|
26
|
-
if lookup
|
|
27
|
-
lookups[i] = Expression.parse(
|
|
31
|
+
if lookup&.start_with?('[') && lookup&.end_with?(']')
|
|
32
|
+
lookups[i] = Expression.parse(
|
|
33
|
+
lookup[1..-2],
|
|
34
|
+
string_scanner,
|
|
35
|
+
cache,
|
|
36
|
+
)
|
|
28
37
|
elsif COMMAND_METHODS.include?(lookup)
|
|
29
38
|
@command_flags |= 1 << i
|
|
30
39
|
end
|
|
31
40
|
end
|
|
32
41
|
end
|
|
33
42
|
|
|
43
|
+
def lookup_command?(lookup_index)
|
|
44
|
+
@command_flags & (1 << lookup_index) != 0
|
|
45
|
+
end
|
|
46
|
+
|
|
34
47
|
def evaluate(context)
|
|
35
|
-
name
|
|
48
|
+
name = context.evaluate(@name)
|
|
36
49
|
object = context.find_variable(name)
|
|
37
50
|
|
|
38
51
|
@lookups.each_index do |i|
|
|
39
52
|
key = context.evaluate(@lookups[i])
|
|
40
53
|
|
|
54
|
+
# Cast "key" to its liquid value to enable it to act as a primitive value
|
|
55
|
+
key = Liquid::Utils.to_liquid_value(key)
|
|
56
|
+
|
|
41
57
|
# If object is a hash- or array-like object we look for the
|
|
42
58
|
# presence of the key and if its available we return it
|
|
43
59
|
if object.respond_to?(:[]) &&
|
|
@@ -45,13 +61,13 @@ module Liquid
|
|
|
45
61
|
(object.respond_to?(:fetch) && key.is_a?(Integer)))
|
|
46
62
|
|
|
47
63
|
# if its a proc we will replace the entry with the proc
|
|
48
|
-
res
|
|
64
|
+
res = context.lookup_and_evaluate(object, key)
|
|
49
65
|
object = res.to_liquid
|
|
50
66
|
|
|
51
67
|
# Some special cases. If the part wasn't in square brackets and
|
|
52
68
|
# no key with the same name was found we interpret following calls
|
|
53
69
|
# as commands and call them on the current object
|
|
54
|
-
elsif
|
|
70
|
+
elsif lookup_command?(i) && object.respond_to?(key)
|
|
55
71
|
object = object.send(key).to_liquid
|
|
56
72
|
|
|
57
73
|
# No key was present with the desired value and it wasn't one of the directly supported
|
|
@@ -78,5 +94,11 @@ module Liquid
|
|
|
78
94
|
def state
|
|
79
95
|
[@name, @lookups, @command_flags]
|
|
80
96
|
end
|
|
97
|
+
|
|
98
|
+
class ParseTreeVisitor < Liquid::ParseTreeVisitor
|
|
99
|
+
def children
|
|
100
|
+
@node.lookups
|
|
101
|
+
end
|
|
102
|
+
end
|
|
81
103
|
end
|
|
82
104
|
end
|