liquid 3.0.0.rc1 → 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/History.md +4 -0
- data/README.md +2 -2
- data/lib/liquid.rb +8 -0
- data/lib/liquid/block.rb +50 -46
- data/lib/liquid/block_body.rb +123 -0
- data/lib/liquid/condition.rb +12 -5
- data/lib/liquid/context.rb +75 -148
- data/lib/liquid/errors.rb +50 -2
- data/lib/liquid/expression.rb +33 -0
- data/lib/liquid/parser_switching.rb +31 -0
- data/lib/liquid/profiler.rb +159 -0
- data/lib/liquid/profiler/hooks.rb +23 -0
- data/lib/liquid/range_lookup.rb +22 -0
- data/lib/liquid/standardfilters.rb +29 -4
- data/lib/liquid/tag.rb +6 -25
- data/lib/liquid/tags/assign.rb +2 -1
- data/lib/liquid/tags/case.rb +1 -1
- data/lib/liquid/tags/if.rb +5 -5
- data/lib/liquid/tags/ifchanged.rb +1 -1
- data/lib/liquid/tags/include.rb +11 -1
- data/lib/liquid/tags/raw.rb +1 -4
- data/lib/liquid/tags/table_row.rb +1 -1
- data/lib/liquid/template.rb +55 -4
- data/lib/liquid/token.rb +18 -0
- data/lib/liquid/variable.rb +68 -41
- data/lib/liquid/variable_lookup.rb +78 -0
- data/lib/liquid/version.rb +1 -1
- data/test/integration/assign_test.rb +12 -1
- data/test/integration/blank_test.rb +1 -1
- data/test/integration/capture_test.rb +1 -1
- data/test/integration/context_test.rb +10 -11
- data/test/integration/drop_test.rb +29 -3
- data/test/integration/error_handling_test.rb +138 -41
- data/test/integration/filter_test.rb +7 -7
- data/test/integration/hash_ordering_test.rb +6 -8
- data/test/integration/output_test.rb +1 -1
- data/test/integration/parsing_quirks_test.rb +40 -18
- data/test/integration/render_profiling_test.rb +154 -0
- data/test/integration/security_test.rb +1 -1
- data/test/integration/standard_filter_test.rb +47 -1
- data/test/integration/tags/break_tag_test.rb +1 -1
- data/test/integration/tags/continue_tag_test.rb +1 -1
- data/test/integration/tags/for_tag_test.rb +2 -2
- data/test/integration/tags/if_else_tag_test.rb +23 -20
- data/test/integration/tags/include_tag_test.rb +24 -2
- data/test/integration/tags/increment_tag_test.rb +1 -1
- data/test/integration/tags/raw_tag_test.rb +1 -1
- data/test/integration/tags/standard_tag_test.rb +4 -4
- data/test/integration/tags/statements_test.rb +1 -1
- data/test/integration/tags/table_row_test.rb +1 -1
- data/test/integration/tags/unless_else_tag_test.rb +1 -1
- data/test/integration/template_test.rb +16 -4
- data/test/integration/variable_test.rb +11 -1
- data/test/test_helper.rb +59 -31
- data/test/unit/block_unit_test.rb +2 -5
- data/test/unit/condition_unit_test.rb +5 -1
- data/test/unit/context_unit_test.rb +13 -7
- data/test/unit/file_system_unit_test.rb +5 -5
- data/test/unit/i18n_unit_test.rb +3 -3
- data/test/unit/lexer_unit_test.rb +1 -1
- data/test/unit/module_ex_unit_test.rb +1 -1
- data/test/unit/parser_unit_test.rb +1 -1
- data/test/unit/regexp_unit_test.rb +1 -1
- data/test/unit/strainer_unit_test.rb +3 -2
- data/test/unit/tag_unit_test.rb +6 -1
- data/test/unit/tags/case_tag_unit_test.rb +1 -1
- data/test/unit/tags/for_tag_unit_test.rb +1 -1
- data/test/unit/tags/if_tag_unit_test.rb +1 -1
- data/test/unit/template_unit_test.rb +1 -1
- data/test/unit/tokenizer_unit_test.rb +10 -1
- data/test/unit/variable_unit_test.rb +49 -46
- metadata +71 -47
data/lib/liquid/tags/assign.rb
CHANGED
data/lib/liquid/tags/case.rb
CHANGED
data/lib/liquid/tags/if.rb
CHANGED
@@ -21,7 +21,7 @@ module Liquid
|
|
21
21
|
end
|
22
22
|
|
23
23
|
def nodelist
|
24
|
-
@blocks.
|
24
|
+
@blocks.flat_map(&:attachment)
|
25
25
|
end
|
26
26
|
|
27
27
|
def unknown_tag(tag, markup, tokens)
|
@@ -57,15 +57,15 @@ module Liquid
|
|
57
57
|
end
|
58
58
|
|
59
59
|
def lax_parse(markup)
|
60
|
-
expressions = markup.scan(ExpressionsAndOperators)
|
61
|
-
raise(SyntaxError.new(options[:locale].t("errors.syntax.if".freeze))) unless expressions.
|
60
|
+
expressions = markup.scan(ExpressionsAndOperators)
|
61
|
+
raise(SyntaxError.new(options[:locale].t("errors.syntax.if".freeze))) unless expressions.pop =~ Syntax
|
62
62
|
|
63
63
|
condition = Condition.new($1, $2, $3)
|
64
64
|
|
65
65
|
while not expressions.empty?
|
66
|
-
operator =
|
66
|
+
operator = expressions.pop.to_s.strip
|
67
67
|
|
68
|
-
raise(SyntaxError.new(options[:locale].t("errors.syntax.if".freeze))) unless expressions.
|
68
|
+
raise(SyntaxError.new(options[:locale].t("errors.syntax.if".freeze))) unless expressions.pop.to_s =~ Syntax
|
69
69
|
|
70
70
|
new_condition = Condition.new($1, $2, $3)
|
71
71
|
raise(SyntaxError.new(options[:locale].t("errors.syntax.if".freeze))) unless BOOLEAN_OPERATORS.include?(operator)
|
data/lib/liquid/tags/include.rb
CHANGED
@@ -69,7 +69,7 @@ module Liquid
|
|
69
69
|
return cached
|
70
70
|
end
|
71
71
|
source = read_template_from_file_system(context)
|
72
|
-
partial = Liquid::Template.parse(source)
|
72
|
+
partial = Liquid::Template.parse(source, pass_options)
|
73
73
|
cached_partials[template_name] = partial
|
74
74
|
context.registers[:cached_partials] = cached_partials
|
75
75
|
partial
|
@@ -88,6 +88,16 @@ module Liquid
|
|
88
88
|
raise ArgumentError, "file_system.read_template_file expects two parameters: (template_name, context)"
|
89
89
|
end
|
90
90
|
end
|
91
|
+
|
92
|
+
def pass_options
|
93
|
+
dont_pass = @options[:include_options_blacklist]
|
94
|
+
return {locale: @options[:locale]} if dont_pass == true
|
95
|
+
opts = @options.merge(included: true, include_options_blacklist: false)
|
96
|
+
if dont_pass.is_a?(Array)
|
97
|
+
dont_pass.each {|o| opts.delete(o)}
|
98
|
+
end
|
99
|
+
opts
|
100
|
+
end
|
91
101
|
end
|
92
102
|
|
93
103
|
Template.register_tag('include'.freeze, Include)
|
data/lib/liquid/tags/raw.rb
CHANGED
@@ -8,10 +8,7 @@ module Liquid
|
|
8
8
|
while token = tokens.shift
|
9
9
|
if token =~ FullTokenPossiblyInvalid
|
10
10
|
@nodelist << $1 if $1 != "".freeze
|
11
|
-
if block_delimiter == $2
|
12
|
-
end_tag
|
13
|
-
return
|
14
|
-
end
|
11
|
+
return if block_delimiter == $2
|
15
12
|
end
|
16
13
|
@nodelist << token if not token.empty?
|
17
14
|
end
|
data/lib/liquid/template.rb
CHANGED
@@ -51,6 +51,8 @@ module Liquid
|
|
51
51
|
end
|
52
52
|
end
|
53
53
|
|
54
|
+
attr_reader :profiler
|
55
|
+
|
54
56
|
class << self
|
55
57
|
# Sets how strict the parser should be.
|
56
58
|
# :lax acts like liquid 2.5 and silently ignores malformed tags in most cases.
|
@@ -58,6 +60,12 @@ module Liquid
|
|
58
60
|
# :strict will enforce correct syntax.
|
59
61
|
attr_writer :error_mode
|
60
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
|
+
|
61
69
|
def file_system
|
62
70
|
@@file_system
|
63
71
|
end
|
@@ -78,27 +86,39 @@ module Liquid
|
|
78
86
|
@error_mode || :lax
|
79
87
|
end
|
80
88
|
|
89
|
+
def taint_mode
|
90
|
+
@taint_mode || :lax
|
91
|
+
end
|
92
|
+
|
81
93
|
# Pass a module with filter methods which should be available
|
82
94
|
# to all liquid views. Good for registering the standard library
|
83
95
|
def register_filter(mod)
|
84
96
|
Strainer.global_filter(mod)
|
85
97
|
end
|
86
98
|
|
99
|
+
def default_resource_limits
|
100
|
+
@default_resource_limits ||= {}
|
101
|
+
end
|
102
|
+
|
87
103
|
# creates a new <tt>Template</tt> object from liquid source code
|
104
|
+
# To enable profiling, pass in <tt>profile: true</tt> as an option.
|
105
|
+
# See Liquid::Profiler for more information
|
88
106
|
def parse(source, options = {})
|
89
107
|
template = Template.new
|
90
108
|
template.parse(source, options)
|
91
109
|
end
|
92
110
|
end
|
93
111
|
|
94
|
-
# creates a new <tt>Template</tt> from an array of tokens. Use <tt>Template.parse</tt> instead
|
95
112
|
def initialize
|
96
|
-
@resource_limits =
|
113
|
+
@resource_limits = self.class.default_resource_limits.dup
|
97
114
|
end
|
98
115
|
|
99
116
|
# Parse source code.
|
100
117
|
# Returns self for easy chaining
|
101
118
|
def parse(source, options = {})
|
119
|
+
@options = options
|
120
|
+
@profiling = options[:profile]
|
121
|
+
@line_numbers = options[:line_numbers] || @profiling
|
102
122
|
@root = Document.parse(tokenize(source), DEFAULT_OPTIONS.merge(options))
|
103
123
|
@warnings = nil
|
104
124
|
self
|
@@ -130,6 +150,9 @@ module Liquid
|
|
130
150
|
# if you use the same filters over and over again consider registering them globally
|
131
151
|
# with <tt>Template.register_filter</tt>
|
132
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
|
+
#
|
133
156
|
# Following options can be passed:
|
134
157
|
#
|
135
158
|
# * <tt>filters</tt> : array with local filters
|
@@ -183,7 +206,9 @@ module Liquid
|
|
183
206
|
begin
|
184
207
|
# render the nodelist.
|
185
208
|
# for performance reasons we get an array back here. join will make a string out of it.
|
186
|
-
result =
|
209
|
+
result = with_profiling do
|
210
|
+
@root.render(context)
|
211
|
+
end
|
187
212
|
result.respond_to?(:join) ? result.join : result
|
188
213
|
rescue Liquid::MemoryError => e
|
189
214
|
context.handle_error(e)
|
@@ -203,7 +228,8 @@ module Liquid
|
|
203
228
|
def tokenize(source)
|
204
229
|
source = source.source if source.respond_to?(:source)
|
205
230
|
return [] if source.to_s.empty?
|
206
|
-
|
231
|
+
|
232
|
+
tokens = calculate_line_numbers(source.split(TemplateParser))
|
207
233
|
|
208
234
|
# removes the rogue empty element at the beginning of the array
|
209
235
|
tokens.shift if tokens[0] and tokens[0].empty?
|
@@ -211,5 +237,30 @@ module Liquid
|
|
211
237
|
tokens
|
212
238
|
end
|
213
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
|
214
265
|
end
|
215
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/variable.rb
CHANGED
@@ -11,40 +11,41 @@ module Liquid
|
|
11
11
|
# {{ user | link }}
|
12
12
|
#
|
13
13
|
class Variable
|
14
|
-
FilterParser = /(
|
14
|
+
FilterParser = /(?:\s+|#{QuotedFragment}|#{ArgumentSeparator})+/o
|
15
15
|
EasyParse = /\A *(\w+(?:\.\w+)*) *\z/
|
16
16
|
attr_accessor :filters, :name, :warnings
|
17
|
+
attr_accessor :line_number
|
18
|
+
include ParserSwitching
|
17
19
|
|
18
20
|
def initialize(markup, options = {})
|
19
21
|
@markup = markup
|
20
22
|
@name = nil
|
21
23
|
@options = options || {}
|
22
24
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
lax_parse(markup)
|
33
|
-
end
|
34
|
-
end
|
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}}}\""
|
35
34
|
end
|
36
35
|
|
37
36
|
def lax_parse(markup)
|
38
37
|
@filters = []
|
39
|
-
if markup =~
|
40
|
-
|
41
|
-
|
42
|
-
|
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)
|
43
44
|
filters.each do |f|
|
44
|
-
if f =~ /\
|
45
|
-
filtername = Regexp.last_match(
|
45
|
+
if f =~ /\w+/
|
46
|
+
filtername = Regexp.last_match(0)
|
46
47
|
filterargs = f.scan(/(?:#{FilterArgumentSeparator}|#{ArgumentSeparator})\s*((?:\w+\s*\:\s*)?#{QuotedFragment})/o).flatten
|
47
|
-
@filters <<
|
48
|
+
@filters << parse_filter_expressions(filtername, filterargs)
|
48
49
|
end
|
49
50
|
end
|
50
51
|
end
|
@@ -54,7 +55,7 @@ module Liquid
|
|
54
55
|
def strict_parse(markup)
|
55
56
|
# Very simple valid cases
|
56
57
|
if markup =~ EasyParse
|
57
|
-
@name = $1
|
58
|
+
@name = Expression.parse($1)
|
58
59
|
@filters = []
|
59
60
|
return
|
60
61
|
end
|
@@ -62,16 +63,13 @@ module Liquid
|
|
62
63
|
@filters = []
|
63
64
|
p = Parser.new(markup)
|
64
65
|
# Could be just filters with no input
|
65
|
-
@name = p.look(:pipe) ?
|
66
|
+
@name = p.look(:pipe) ? nil : Expression.parse(p.expression)
|
66
67
|
while p.consume?(:pipe)
|
67
68
|
filtername = p.consume(:id)
|
68
69
|
filterargs = p.consume?(:colon) ? parse_filterargs(p) : []
|
69
|
-
@filters <<
|
70
|
+
@filters << parse_filter_expressions(filtername, filterargs)
|
70
71
|
end
|
71
72
|
p.consume(:end_of_string)
|
72
|
-
rescue SyntaxError => e
|
73
|
-
e.message << " in \"{{#{markup}}}\""
|
74
|
-
raise e
|
75
73
|
end
|
76
74
|
|
77
75
|
def parse_filterargs(p)
|
@@ -85,22 +83,51 @@ module Liquid
|
|
85
83
|
end
|
86
84
|
|
87
85
|
def render(context)
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
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)
|
102
|
+
end
|
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)
|
98
115
|
end
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
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"
|
104
131
|
end
|
105
132
|
end
|
106
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
|