liquid 3.0.0.rc1 → 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 +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
|