liquid 2.6.3 → 3.0.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 +4 -4
- data/History.md +46 -13
- data/README.md +27 -2
- data/lib/liquid/block.rb +85 -51
- data/lib/liquid/block_body.rb +123 -0
- data/lib/liquid/condition.rb +26 -15
- data/lib/liquid/context.rb +106 -140
- data/lib/liquid/document.rb +3 -3
- data/lib/liquid/drop.rb +17 -1
- data/lib/liquid/errors.rb +50 -2
- data/lib/liquid/expression.rb +33 -0
- data/lib/liquid/file_system.rb +17 -6
- data/lib/liquid/i18n.rb +39 -0
- data/lib/liquid/interrupts.rb +1 -1
- data/lib/liquid/lexer.rb +51 -0
- data/lib/liquid/locales/en.yml +22 -0
- data/lib/liquid/parser.rb +90 -0
- data/lib/liquid/parser_switching.rb +31 -0
- data/lib/liquid/profiler/hooks.rb +23 -0
- data/lib/liquid/profiler.rb +159 -0
- data/lib/liquid/range_lookup.rb +22 -0
- data/lib/liquid/standardfilters.rb +143 -55
- data/lib/liquid/strainer.rb +14 -4
- data/lib/liquid/tag.rb +25 -9
- data/lib/liquid/tags/assign.rb +12 -9
- data/lib/liquid/tags/break.rb +1 -1
- data/lib/liquid/tags/capture.rb +10 -8
- data/lib/liquid/tags/case.rb +13 -13
- data/lib/liquid/tags/comment.rb +9 -2
- data/lib/liquid/tags/continue.rb +1 -4
- data/lib/liquid/tags/cycle.rb +5 -7
- data/lib/liquid/tags/decrement.rb +3 -4
- data/lib/liquid/tags/for.rb +69 -36
- data/lib/liquid/tags/if.rb +52 -25
- data/lib/liquid/tags/ifchanged.rb +3 -3
- data/lib/liquid/tags/include.rb +19 -8
- data/lib/liquid/tags/increment.rb +4 -8
- data/lib/liquid/tags/raw.rb +4 -7
- data/lib/liquid/tags/table_row.rb +73 -0
- data/lib/liquid/tags/unless.rb +2 -4
- data/lib/liquid/template.rb +124 -14
- data/lib/liquid/token.rb +18 -0
- data/lib/liquid/utils.rb +13 -4
- data/lib/liquid/variable.rb +103 -25
- data/lib/liquid/variable_lookup.rb +78 -0
- data/lib/liquid/version.rb +1 -1
- data/lib/liquid.rb +19 -11
- data/test/fixtures/en_locale.yml +9 -0
- data/test/{liquid → integration}/assign_test.rb +18 -1
- data/test/integration/blank_test.rb +106 -0
- data/test/{liquid → integration}/capture_test.rb +3 -3
- data/test/integration/context_test.rb +32 -0
- data/test/integration/drop_test.rb +271 -0
- data/test/integration/error_handling_test.rb +207 -0
- data/test/{liquid → integration}/filter_test.rb +11 -11
- data/test/integration/hash_ordering_test.rb +23 -0
- data/test/{liquid → integration}/output_test.rb +13 -13
- data/test/integration/parsing_quirks_test.rb +116 -0
- data/test/integration/render_profiling_test.rb +154 -0
- data/test/{liquid → integration}/security_test.rb +10 -10
- data/test/{liquid → integration}/standard_filter_test.rb +148 -32
- data/test/{liquid → integration}/tags/break_tag_test.rb +1 -1
- data/test/{liquid → integration}/tags/continue_tag_test.rb +1 -1
- data/test/{liquid → integration}/tags/for_tag_test.rb +80 -2
- data/test/{liquid → integration}/tags/if_else_tag_test.rb +24 -21
- data/test/integration/tags/include_tag_test.rb +234 -0
- data/test/{liquid → integration}/tags/increment_tag_test.rb +1 -1
- data/test/{liquid → integration}/tags/raw_tag_test.rb +2 -1
- data/test/{liquid → integration}/tags/standard_tag_test.rb +28 -26
- data/test/integration/tags/statements_test.rb +113 -0
- data/test/{liquid/tags/html_tag_test.rb → integration/tags/table_row_test.rb} +5 -5
- data/test/{liquid → integration}/tags/unless_else_tag_test.rb +1 -1
- data/test/{liquid → integration}/template_test.rb +81 -45
- data/test/integration/variable_test.rb +82 -0
- data/test/test_helper.rb +73 -20
- data/test/{liquid/block_test.rb → unit/block_unit_test.rb} +2 -5
- data/test/{liquid/condition_test.rb → unit/condition_unit_test.rb} +23 -1
- data/test/{liquid/context_test.rb → unit/context_unit_test.rb} +39 -25
- data/test/{liquid/file_system_test.rb → unit/file_system_unit_test.rb} +11 -5
- data/test/unit/i18n_unit_test.rb +37 -0
- data/test/unit/lexer_unit_test.rb +48 -0
- data/test/{liquid/module_ex_test.rb → unit/module_ex_unit_test.rb} +7 -7
- data/test/unit/parser_unit_test.rb +82 -0
- data/test/{liquid/regexp_test.rb → unit/regexp_unit_test.rb} +3 -3
- data/test/{liquid/strainer_test.rb → unit/strainer_unit_test.rb} +20 -1
- data/test/unit/tag_unit_test.rb +16 -0
- data/test/unit/tags/case_tag_unit_test.rb +10 -0
- data/test/unit/tags/for_tag_unit_test.rb +13 -0
- data/test/unit/tags/if_tag_unit_test.rb +8 -0
- data/test/unit/template_unit_test.rb +69 -0
- data/test/unit/tokenizer_unit_test.rb +38 -0
- data/test/unit/variable_unit_test.rb +139 -0
- metadata +135 -67
- data/lib/extras/liquid_view.rb +0 -51
- data/lib/liquid/htmltags.rb +0 -73
- data/test/liquid/drop_test.rb +0 -180
- data/test/liquid/error_handling_test.rb +0 -81
- data/test/liquid/hash_ordering_test.rb +0 -25
- data/test/liquid/parsing_quirks_test.rb +0 -52
- data/test/liquid/tags/include_tag_test.rb +0 -166
- data/test/liquid/tags/statements_test.rb +0 -134
- data/test/liquid/variable_test.rb +0 -186
@@ -1,12 +1,11 @@
|
|
1
1
|
module Liquid
|
2
|
-
|
3
2
|
# increment is used in a place where one needs to insert a counter
|
4
3
|
# into a template, and needs the counter to survive across
|
5
4
|
# multiple instantiations of the template.
|
6
5
|
# (To achieve the survival, the application must keep the context)
|
7
6
|
#
|
8
7
|
# if the variable does not exist, it is created with value 0.
|
9
|
-
|
8
|
+
#
|
10
9
|
# Hello: {% increment variable %}
|
11
10
|
#
|
12
11
|
# gives you:
|
@@ -16,10 +15,9 @@ module Liquid
|
|
16
15
|
# Hello: 2
|
17
16
|
#
|
18
17
|
class Increment < Tag
|
19
|
-
def initialize(tag_name, markup,
|
20
|
-
@variable = markup.strip
|
21
|
-
|
18
|
+
def initialize(tag_name, markup, options)
|
22
19
|
super
|
20
|
+
@variable = markup.strip
|
23
21
|
end
|
24
22
|
|
25
23
|
def render(context)
|
@@ -27,9 +25,7 @@ module Liquid
|
|
27
25
|
context.environments.first[@variable] = value + 1
|
28
26
|
value.to_s
|
29
27
|
end
|
30
|
-
|
31
|
-
private
|
32
28
|
end
|
33
29
|
|
34
|
-
Template.register_tag('increment', Increment)
|
30
|
+
Template.register_tag('increment'.freeze, Increment)
|
35
31
|
end
|
data/lib/liquid/tags/raw.rb
CHANGED
@@ -1,22 +1,19 @@
|
|
1
1
|
module Liquid
|
2
2
|
class Raw < Block
|
3
|
-
FullTokenPossiblyInvalid =
|
3
|
+
FullTokenPossiblyInvalid = /\A(.*)#{TagStart}\s*(\w+)\s*(.*)?#{TagEnd}\z/om
|
4
4
|
|
5
5
|
def parse(tokens)
|
6
6
|
@nodelist ||= []
|
7
7
|
@nodelist.clear
|
8
8
|
while token = tokens.shift
|
9
9
|
if token =~ FullTokenPossiblyInvalid
|
10
|
-
@nodelist << $1 if $1 != ""
|
11
|
-
if block_delimiter == $2
|
12
|
-
end_tag
|
13
|
-
return
|
14
|
-
end
|
10
|
+
@nodelist << $1 if $1 != "".freeze
|
11
|
+
return if block_delimiter == $2
|
15
12
|
end
|
16
13
|
@nodelist << token if not token.empty?
|
17
14
|
end
|
18
15
|
end
|
19
16
|
end
|
20
17
|
|
21
|
-
Template.register_tag('raw', Raw)
|
18
|
+
Template.register_tag('raw'.freeze, Raw)
|
22
19
|
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module Liquid
|
2
|
+
class TableRow < Block
|
3
|
+
Syntax = /(\w+)\s+in\s+(#{QuotedFragment}+)/o
|
4
|
+
|
5
|
+
def initialize(tag_name, markup, options)
|
6
|
+
super
|
7
|
+
if markup =~ Syntax
|
8
|
+
@variable_name = $1
|
9
|
+
@collection_name = $2
|
10
|
+
@attributes = {}
|
11
|
+
markup.scan(TagAttributes) do |key, value|
|
12
|
+
@attributes[key] = value
|
13
|
+
end
|
14
|
+
else
|
15
|
+
raise SyntaxError.new(options[:locale].t("errors.syntax.table_row".freeze))
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def render(context)
|
20
|
+
collection = context[@collection_name] or return ''.freeze
|
21
|
+
|
22
|
+
from = @attributes['offset'.freeze] ? context[@attributes['offset'.freeze]].to_i : 0
|
23
|
+
to = @attributes['limit'.freeze] ? from + context[@attributes['limit'.freeze]].to_i : nil
|
24
|
+
|
25
|
+
collection = Utils.slice_collection(collection, from, to)
|
26
|
+
|
27
|
+
length = collection.length
|
28
|
+
|
29
|
+
cols = context[@attributes['cols'.freeze]].to_i
|
30
|
+
|
31
|
+
row = 1
|
32
|
+
col = 0
|
33
|
+
|
34
|
+
result = "<tr class=\"row1\">\n"
|
35
|
+
context.stack do
|
36
|
+
|
37
|
+
collection.each_with_index do |item, index|
|
38
|
+
context[@variable_name] = item
|
39
|
+
context['tablerowloop'.freeze] = {
|
40
|
+
'length'.freeze => length,
|
41
|
+
'index'.freeze => index + 1,
|
42
|
+
'index0'.freeze => index,
|
43
|
+
'col'.freeze => col + 1,
|
44
|
+
'col0'.freeze => col,
|
45
|
+
'index0'.freeze => index,
|
46
|
+
'rindex'.freeze => length - index,
|
47
|
+
'rindex0'.freeze => length - index - 1,
|
48
|
+
'first'.freeze => (index == 0),
|
49
|
+
'last'.freeze => (index == length - 1),
|
50
|
+
'col_first'.freeze => (col == 0),
|
51
|
+
'col_last'.freeze => (col == cols - 1)
|
52
|
+
}
|
53
|
+
|
54
|
+
|
55
|
+
col += 1
|
56
|
+
|
57
|
+
result << "<td class=\"col#{col}\">" << super << '</td>'
|
58
|
+
|
59
|
+
if col == cols and (index != length - 1)
|
60
|
+
col = 0
|
61
|
+
row += 1
|
62
|
+
result << "</tr>\n<tr class=\"row#{row}\">"
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
end
|
67
|
+
result << "</tr>\n"
|
68
|
+
result
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
Template.register_tag('tablerow'.freeze, TableRow)
|
73
|
+
end
|
data/lib/liquid/tags/unless.rb
CHANGED
@@ -1,7 +1,6 @@
|
|
1
1
|
require File.dirname(__FILE__) + '/if'
|
2
2
|
|
3
3
|
module Liquid
|
4
|
-
|
5
4
|
# Unless is a conditional just like 'if' but works on the inverse logic.
|
6
5
|
#
|
7
6
|
# {% unless x < 0 %} x is greater than zero {% end %}
|
@@ -23,11 +22,10 @@ module Liquid
|
|
23
22
|
end
|
24
23
|
end
|
25
24
|
|
26
|
-
''
|
25
|
+
''.freeze
|
27
26
|
end
|
28
27
|
end
|
29
28
|
end
|
30
29
|
|
31
|
-
|
32
|
-
Template.register_tag('unless', Unless)
|
30
|
+
Template.register_tag('unless'.freeze, Unless)
|
33
31
|
end
|
data/lib/liquid/template.rb
CHANGED
@@ -14,10 +14,58 @@ module Liquid
|
|
14
14
|
# template.render('user_name' => 'bob')
|
15
15
|
#
|
16
16
|
class Template
|
17
|
+
DEFAULT_OPTIONS = {
|
18
|
+
:locale => I18n.new
|
19
|
+
}
|
20
|
+
|
17
21
|
attr_accessor :root, :resource_limits
|
18
22
|
@@file_system = BlankFileSystem.new
|
19
23
|
|
24
|
+
class TagRegistry
|
25
|
+
def initialize
|
26
|
+
@tags = {}
|
27
|
+
@cache = {}
|
28
|
+
end
|
29
|
+
|
30
|
+
def [](tag_name)
|
31
|
+
return nil unless @tags.has_key?(tag_name)
|
32
|
+
return @cache[tag_name] if Liquid.cache_classes
|
33
|
+
|
34
|
+
lookup_class(@tags[tag_name]).tap { |o| @cache[tag_name] = o }
|
35
|
+
end
|
36
|
+
|
37
|
+
def []=(tag_name, klass)
|
38
|
+
@tags[tag_name] = klass.name
|
39
|
+
@cache[tag_name] = klass
|
40
|
+
end
|
41
|
+
|
42
|
+
def delete(tag_name)
|
43
|
+
@tags.delete(tag_name)
|
44
|
+
@cache.delete(tag_name)
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def lookup_class(name)
|
50
|
+
name.split("::").reject(&:empty?).reduce(Object) { |scope, const| scope.const_get(const) }
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
attr_reader :profiler
|
55
|
+
|
20
56
|
class << self
|
57
|
+
# Sets how strict the parser should be.
|
58
|
+
# :lax acts like liquid 2.5 and silently ignores malformed tags in most cases.
|
59
|
+
# :warn is the default and will give deprecation warnings when invalid syntax is used.
|
60
|
+
# :strict will enforce correct syntax.
|
61
|
+
attr_writer :error_mode
|
62
|
+
|
63
|
+
# Sets how strict the taint checker should be.
|
64
|
+
# :lax is the default, and ignores the taint flag completely
|
65
|
+
# :warn adds a warning, but does not interrupt the rendering
|
66
|
+
# :error raises an error when tainted output is used
|
67
|
+
attr_writer :taint_mode
|
68
|
+
|
21
69
|
def file_system
|
22
70
|
@@file_system
|
23
71
|
end
|
@@ -31,7 +79,15 @@ module Liquid
|
|
31
79
|
end
|
32
80
|
|
33
81
|
def tags
|
34
|
-
@tags ||=
|
82
|
+
@tags ||= TagRegistry.new
|
83
|
+
end
|
84
|
+
|
85
|
+
def error_mode
|
86
|
+
@error_mode || :lax
|
87
|
+
end
|
88
|
+
|
89
|
+
def taint_mode
|
90
|
+
@taint_mode || :lax
|
35
91
|
end
|
36
92
|
|
37
93
|
# Pass a module with filter methods which should be available
|
@@ -40,26 +96,39 @@ module Liquid
|
|
40
96
|
Strainer.global_filter(mod)
|
41
97
|
end
|
42
98
|
|
99
|
+
def default_resource_limits
|
100
|
+
@default_resource_limits ||= {}
|
101
|
+
end
|
102
|
+
|
43
103
|
# creates a new <tt>Template</tt> object from liquid source code
|
44
|
-
|
104
|
+
# To enable profiling, pass in <tt>profile: true</tt> as an option.
|
105
|
+
# See Liquid::Profiler for more information
|
106
|
+
def parse(source, options = {})
|
45
107
|
template = Template.new
|
46
|
-
template.parse(source)
|
47
|
-
template
|
108
|
+
template.parse(source, options)
|
48
109
|
end
|
49
110
|
end
|
50
111
|
|
51
|
-
# creates a new <tt>Template</tt> from an array of tokens. Use <tt>Template.parse</tt> instead
|
52
112
|
def initialize
|
53
|
-
@resource_limits =
|
113
|
+
@resource_limits = self.class.default_resource_limits.dup
|
54
114
|
end
|
55
115
|
|
56
116
|
# Parse source code.
|
57
117
|
# Returns self for easy chaining
|
58
|
-
def parse(source)
|
59
|
-
@
|
118
|
+
def parse(source, options = {})
|
119
|
+
@options = options
|
120
|
+
@profiling = options[:profile]
|
121
|
+
@line_numbers = options[:line_numbers] || @profiling
|
122
|
+
@root = Document.parse(tokenize(source), DEFAULT_OPTIONS.merge(options))
|
123
|
+
@warnings = nil
|
60
124
|
self
|
61
125
|
end
|
62
126
|
|
127
|
+
def warnings
|
128
|
+
return [] unless @root
|
129
|
+
@warnings ||= @root.warnings
|
130
|
+
end
|
131
|
+
|
63
132
|
def registers
|
64
133
|
@registers ||= {}
|
65
134
|
end
|
@@ -81,6 +150,9 @@ module Liquid
|
|
81
150
|
# if you use the same filters over and over again consider registering them globally
|
82
151
|
# with <tt>Template.register_filter</tt>
|
83
152
|
#
|
153
|
+
# if profiling was enabled in <tt>Template#parse</tt> then the resulting profiling information
|
154
|
+
# will be available via <tt>Template#profiler</tt>
|
155
|
+
#
|
84
156
|
# Following options can be passed:
|
85
157
|
#
|
86
158
|
# * <tt>filters</tt> : array with local filters
|
@@ -88,11 +160,17 @@ module Liquid
|
|
88
160
|
# filters and tags and might be useful to integrate liquid more with its host application
|
89
161
|
#
|
90
162
|
def render(*args)
|
91
|
-
return '' if @root.nil?
|
163
|
+
return ''.freeze if @root.nil?
|
92
164
|
|
93
165
|
context = case args.first
|
94
166
|
when Liquid::Context
|
95
|
-
args.shift
|
167
|
+
c = args.shift
|
168
|
+
|
169
|
+
if @rethrow_errors
|
170
|
+
c.exception_handler = ->(e) { true }
|
171
|
+
end
|
172
|
+
|
173
|
+
c
|
96
174
|
when Liquid::Drop
|
97
175
|
drop = args.shift
|
98
176
|
drop.context = Context.new([drop, assigns], instance_assigns, registers, @rethrow_errors, @resource_limits)
|
@@ -101,7 +179,7 @@ module Liquid
|
|
101
179
|
when nil
|
102
180
|
Context.new(assigns, instance_assigns, registers, @rethrow_errors, @resource_limits)
|
103
181
|
else
|
104
|
-
raise ArgumentError, "
|
182
|
+
raise ArgumentError, "Expected Hash or Liquid::Context as parameter"
|
105
183
|
end
|
106
184
|
|
107
185
|
case args.last
|
@@ -116,6 +194,9 @@ module Liquid
|
|
116
194
|
context.add_filters(options[:filters])
|
117
195
|
end
|
118
196
|
|
197
|
+
if options[:exception_handler]
|
198
|
+
context.exception_handler = options[:exception_handler]
|
199
|
+
end
|
119
200
|
when Module
|
120
201
|
context.add_filters(args.pop)
|
121
202
|
when Array
|
@@ -125,7 +206,9 @@ module Liquid
|
|
125
206
|
begin
|
126
207
|
# render the nodelist.
|
127
208
|
# for performance reasons we get an array back here. join will make a string out of it.
|
128
|
-
result =
|
209
|
+
result = with_profiling do
|
210
|
+
@root.render(context)
|
211
|
+
end
|
129
212
|
result.respond_to?(:join) ? result.join : result
|
130
213
|
rescue Liquid::MemoryError => e
|
131
214
|
context.handle_error(e)
|
@@ -135,7 +218,8 @@ module Liquid
|
|
135
218
|
end
|
136
219
|
|
137
220
|
def render!(*args)
|
138
|
-
@rethrow_errors = true
|
221
|
+
@rethrow_errors = true
|
222
|
+
render(*args)
|
139
223
|
end
|
140
224
|
|
141
225
|
private
|
@@ -144,7 +228,8 @@ module Liquid
|
|
144
228
|
def tokenize(source)
|
145
229
|
source = source.source if source.respond_to?(:source)
|
146
230
|
return [] if source.to_s.empty?
|
147
|
-
|
231
|
+
|
232
|
+
tokens = calculate_line_numbers(source.split(TemplateParser))
|
148
233
|
|
149
234
|
# removes the rogue empty element at the beginning of the array
|
150
235
|
tokens.shift if tokens[0] and tokens[0].empty?
|
@@ -152,5 +237,30 @@ module Liquid
|
|
152
237
|
tokens
|
153
238
|
end
|
154
239
|
|
240
|
+
def calculate_line_numbers(raw_tokens)
|
241
|
+
return raw_tokens unless @line_numbers
|
242
|
+
|
243
|
+
current_line = 1
|
244
|
+
raw_tokens.map do |token|
|
245
|
+
Token.new(token, current_line).tap do
|
246
|
+
current_line += token.count("\n")
|
247
|
+
end
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
def with_profiling
|
252
|
+
if @profiling && !@options[:included]
|
253
|
+
@profiler = Profiler.new
|
254
|
+
@profiler.start
|
255
|
+
|
256
|
+
begin
|
257
|
+
yield
|
258
|
+
ensure
|
259
|
+
@profiler.stop
|
260
|
+
end
|
261
|
+
else
|
262
|
+
yield
|
263
|
+
end
|
264
|
+
end
|
155
265
|
end
|
156
266
|
end
|
data/lib/liquid/token.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
module Liquid
|
2
|
+
class Token < String
|
3
|
+
attr_reader :line_number
|
4
|
+
|
5
|
+
def initialize(content, line_number)
|
6
|
+
super(content)
|
7
|
+
@line_number = line_number
|
8
|
+
end
|
9
|
+
|
10
|
+
def raw
|
11
|
+
"<raw>"
|
12
|
+
end
|
13
|
+
|
14
|
+
def child(string)
|
15
|
+
Token.new(string, @line_number)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
data/lib/liquid/utils.rb
CHANGED
@@ -1,5 +1,18 @@
|
|
1
1
|
module Liquid
|
2
2
|
module Utils
|
3
|
+
|
4
|
+
def self.slice_collection(collection, from, to)
|
5
|
+
if (from != 0 || to != nil) && collection.respond_to?(:load_slice)
|
6
|
+
collection.load_slice(from, to)
|
7
|
+
else
|
8
|
+
slice_collection_using_each(collection, from, to)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.non_blank_string?(collection)
|
13
|
+
collection.is_a?(String) && collection != ''.freeze
|
14
|
+
end
|
15
|
+
|
3
16
|
def self.slice_collection_using_each(collection, from, to)
|
4
17
|
segments = []
|
5
18
|
index = 0
|
@@ -22,9 +35,5 @@ module Liquid
|
|
22
35
|
|
23
36
|
segments
|
24
37
|
end
|
25
|
-
|
26
|
-
def self.non_blank_string?(collection)
|
27
|
-
collection.is_a?(String) && collection != ''
|
28
|
-
end
|
29
38
|
end
|
30
39
|
end
|
data/lib/liquid/variable.rb
CHANGED
@@ -11,45 +11,123 @@ module Liquid
|
|
11
11
|
# {{ user | link }}
|
12
12
|
#
|
13
13
|
class Variable
|
14
|
-
FilterParser = /(
|
15
|
-
|
14
|
+
FilterParser = /(?:\s+|#{QuotedFragment}|#{ArgumentSeparator})+/o
|
15
|
+
EasyParse = /\A *(\w+(?:\.\w+)*) *\z/
|
16
|
+
attr_accessor :filters, :name, :warnings
|
17
|
+
attr_accessor :line_number
|
18
|
+
include ParserSwitching
|
16
19
|
|
17
|
-
def initialize(markup)
|
20
|
+
def initialize(markup, options = {})
|
18
21
|
@markup = markup
|
19
22
|
@name = nil
|
23
|
+
@options = options || {}
|
24
|
+
|
25
|
+
parse_with_selected_parser(markup)
|
26
|
+
end
|
27
|
+
|
28
|
+
def raw
|
29
|
+
@markup
|
30
|
+
end
|
31
|
+
|
32
|
+
def markup_context(markup)
|
33
|
+
"in \"{{#{markup}}}\""
|
34
|
+
end
|
35
|
+
|
36
|
+
def lax_parse(markup)
|
20
37
|
@filters = []
|
21
|
-
if
|
22
|
-
|
23
|
-
|
24
|
-
|
38
|
+
if markup =~ /(#{QuotedFragment})(.*)/om
|
39
|
+
name_markup = $1
|
40
|
+
filter_markup = $2
|
41
|
+
@name = Expression.parse(name_markup)
|
42
|
+
if filter_markup =~ /#{FilterSeparator}\s*(.*)/om
|
43
|
+
filters = $1.scan(FilterParser)
|
25
44
|
filters.each do |f|
|
26
|
-
if
|
27
|
-
filtername =
|
45
|
+
if f =~ /\w+/
|
46
|
+
filtername = Regexp.last_match(0)
|
28
47
|
filterargs = f.scan(/(?:#{FilterArgumentSeparator}|#{ArgumentSeparator})\s*((?:\w+\s*\:\s*)?#{QuotedFragment})/o).flatten
|
29
|
-
@filters <<
|
48
|
+
@filters << parse_filter_expressions(filtername, filterargs)
|
30
49
|
end
|
31
50
|
end
|
32
51
|
end
|
33
52
|
end
|
34
53
|
end
|
35
54
|
|
55
|
+
def strict_parse(markup)
|
56
|
+
# Very simple valid cases
|
57
|
+
if markup =~ EasyParse
|
58
|
+
@name = Expression.parse($1)
|
59
|
+
@filters = []
|
60
|
+
return
|
61
|
+
end
|
62
|
+
|
63
|
+
@filters = []
|
64
|
+
p = Parser.new(markup)
|
65
|
+
# Could be just filters with no input
|
66
|
+
@name = p.look(:pipe) ? nil : Expression.parse(p.expression)
|
67
|
+
while p.consume?(:pipe)
|
68
|
+
filtername = p.consume(:id)
|
69
|
+
filterargs = p.consume?(:colon) ? parse_filterargs(p) : []
|
70
|
+
@filters << parse_filter_expressions(filtername, filterargs)
|
71
|
+
end
|
72
|
+
p.consume(:end_of_string)
|
73
|
+
end
|
74
|
+
|
75
|
+
def parse_filterargs(p)
|
76
|
+
# first argument
|
77
|
+
filterargs = [p.argument]
|
78
|
+
# followed by comma separated others
|
79
|
+
while p.consume?(:comma)
|
80
|
+
filterargs << p.argument
|
81
|
+
end
|
82
|
+
filterargs
|
83
|
+
end
|
84
|
+
|
36
85
|
def render(context)
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
86
|
+
@filters.inject(context.evaluate(@name)) do |output, (filter_name, filter_args, filter_kwargs)|
|
87
|
+
filter_args = evaluate_filter_expressions(context, filter_args, filter_kwargs)
|
88
|
+
output = context.invoke(filter_name, output, *filter_args)
|
89
|
+
end.tap{ |obj| taint_check(obj) }
|
90
|
+
end
|
91
|
+
|
92
|
+
private
|
93
|
+
|
94
|
+
def parse_filter_expressions(filter_name, unparsed_args)
|
95
|
+
filter_args = []
|
96
|
+
keyword_args = {}
|
97
|
+
unparsed_args.each do |a|
|
98
|
+
if matches = a.match(/\A#{TagAttributes}\z/o)
|
99
|
+
keyword_args[matches[1]] = Expression.parse(matches[2])
|
100
|
+
else
|
101
|
+
filter_args << Expression.parse(a)
|
47
102
|
end
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
103
|
+
end
|
104
|
+
result = [filter_name, filter_args]
|
105
|
+
result << keyword_args unless keyword_args.empty?
|
106
|
+
result
|
107
|
+
end
|
108
|
+
|
109
|
+
def evaluate_filter_expressions(context, filter_args, filter_kwargs)
|
110
|
+
parsed_args = filter_args.map{ |expr| context.evaluate(expr) }
|
111
|
+
if filter_kwargs
|
112
|
+
parsed_kwargs = {}
|
113
|
+
filter_kwargs.each do |key, expr|
|
114
|
+
parsed_kwargs[key] = context.evaluate(expr)
|
115
|
+
end
|
116
|
+
parsed_args << parsed_kwargs
|
117
|
+
end
|
118
|
+
parsed_args
|
119
|
+
end
|
120
|
+
|
121
|
+
def taint_check(obj)
|
122
|
+
if obj.tainted?
|
123
|
+
@markup =~ QuotedFragment
|
124
|
+
name = Regexp.last_match(0)
|
125
|
+
case Template.taint_mode
|
126
|
+
when :warn
|
127
|
+
@warnings ||= []
|
128
|
+
@warnings << "variable '#{name}' is tainted and was not escaped"
|
129
|
+
when :error
|
130
|
+
raise TaintedError, "Error - variable '#{name}' is tainted and was not escaped"
|
53
131
|
end
|
54
132
|
end
|
55
133
|
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
module Liquid
|
2
|
+
class VariableLookup
|
3
|
+
SQUARE_BRACKETED = /\A\[(.*)\]\z/m
|
4
|
+
COMMAND_METHODS = ['size'.freeze, 'first'.freeze, 'last'.freeze]
|
5
|
+
|
6
|
+
def self.parse(markup)
|
7
|
+
new(markup)
|
8
|
+
end
|
9
|
+
|
10
|
+
def initialize(markup)
|
11
|
+
lookups = markup.scan(VariableParser)
|
12
|
+
|
13
|
+
name = lookups.shift
|
14
|
+
if name =~ SQUARE_BRACKETED
|
15
|
+
name = Expression.parse($1)
|
16
|
+
end
|
17
|
+
@name = name
|
18
|
+
|
19
|
+
@lookups = lookups
|
20
|
+
@command_flags = 0
|
21
|
+
|
22
|
+
@lookups.each_index do |i|
|
23
|
+
lookup = lookups[i]
|
24
|
+
if lookup =~ SQUARE_BRACKETED
|
25
|
+
lookups[i] = Expression.parse($1)
|
26
|
+
elsif COMMAND_METHODS.include?(lookup)
|
27
|
+
@command_flags |= 1 << i
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def evaluate(context)
|
33
|
+
name = context.evaluate(@name)
|
34
|
+
object = context.find_variable(name)
|
35
|
+
|
36
|
+
@lookups.each_index do |i|
|
37
|
+
key = context.evaluate(@lookups[i])
|
38
|
+
|
39
|
+
# If object is a hash- or array-like object we look for the
|
40
|
+
# presence of the key and if its available we return it
|
41
|
+
if object.respond_to?(:[]) &&
|
42
|
+
((object.respond_to?(:has_key?) && object.has_key?(key)) ||
|
43
|
+
(object.respond_to?(:fetch) && key.is_a?(Integer)))
|
44
|
+
|
45
|
+
# if its a proc we will replace the entry with the proc
|
46
|
+
res = context.lookup_and_evaluate(object, key)
|
47
|
+
object = res.to_liquid
|
48
|
+
|
49
|
+
# Some special cases. If the part wasn't in square brackets and
|
50
|
+
# no key with the same name was found we interpret following calls
|
51
|
+
# as commands and call them on the current object
|
52
|
+
elsif @command_flags & (1 << i) != 0 && object.respond_to?(key)
|
53
|
+
object = object.send(key).to_liquid
|
54
|
+
|
55
|
+
# No key was present with the desired value and it wasn't one of the directly supported
|
56
|
+
# keywords either. The only thing we got left is to return nil
|
57
|
+
else
|
58
|
+
return nil
|
59
|
+
end
|
60
|
+
|
61
|
+
# If we are dealing with a drop here we have to
|
62
|
+
object.context = context if object.respond_to?(:context=)
|
63
|
+
end
|
64
|
+
|
65
|
+
object
|
66
|
+
end
|
67
|
+
|
68
|
+
def ==(other)
|
69
|
+
self.class == other.class && self.state == other.state
|
70
|
+
end
|
71
|
+
|
72
|
+
protected
|
73
|
+
|
74
|
+
def state
|
75
|
+
[@name, @lookups, @command_flags]
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
data/lib/liquid/version.rb
CHANGED