liquid 4.0.0 → 4.0.1
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 +5 -2
- data/README.md +2 -0
- data/lib/liquid/block.rb +22 -12
- data/lib/liquid/block_body.rb +62 -59
- data/lib/liquid/condition.rb +20 -10
- data/lib/liquid/context.rb +10 -8
- data/lib/liquid/expression.rb +10 -6
- data/lib/liquid/extensions.rb +6 -0
- data/lib/liquid/lexer.rb +5 -3
- data/lib/liquid/locales/en.yml +1 -1
- data/lib/liquid/parse_context.rb +2 -1
- data/lib/liquid/profiler/hooks.rb +4 -4
- data/lib/liquid/standardfilters.rb +47 -11
- data/lib/liquid/strainer.rb +1 -1
- data/lib/liquid/tags/cycle.rb +2 -2
- data/lib/liquid/tags/for.rb +6 -3
- data/lib/liquid/tags/if.rb +8 -5
- data/lib/liquid/tags/include.rb +1 -1
- data/lib/liquid/tags/table_row.rb +1 -1
- data/lib/liquid/utils.rb +2 -2
- data/lib/liquid/variable.rb +10 -4
- data/lib/liquid/version.rb +1 -1
- data/test/integration/block_test.rb +12 -0
- data/test/integration/parsing_quirks_test.rb +4 -0
- data/test/integration/security_test.rb +14 -0
- data/test/integration/standard_filter_test.rb +97 -6
- data/test/integration/tags/for_tag_test.rb +2 -2
- data/test/integration/tags/include_tag_test.rb +8 -1
- data/test/integration/template_test.rb +9 -0
- data/test/integration/trim_mode_test.rb +4 -0
- data/test/integration/variable_test.rb +4 -0
- data/test/test_helper.rb +0 -1
- data/test/unit/condition_unit_test.rb +8 -0
- data/test/unit/context_unit_test.rb +23 -17
- data/test/unit/lexer_unit_test.rb +1 -1
- data/test/unit/strainer_unit_test.rb +16 -0
- metadata +42 -40
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 55d4aa3aeeef9a99c5601c5c8bbaab62b0c909c9aa3df0ac181e01373a6ed069
|
4
|
+
data.tar.gz: 1e3eddbb13e867eca4f90efd32fbbc2609e4235b77d005efaa93711f8e476c40
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 349a44c983e69443a0350d3aa7de2fffed15bf67356a6ce490583413d60dd926cc32907b1fca4d04ca075dd92e17434176f94552237ec5ae549477ccbbff4042
|
7
|
+
data.tar.gz: 31d9c4fbe841ad2c3a6549e1e6d02f580b31a538186e83140bd297fb47caf23387795a48468886e82562f661231890cb42637ecbe369dc4583d560055e94ca77
|
data/History.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# Liquid Change Log
|
2
2
|
|
3
|
-
## 4.0.0 /
|
3
|
+
## 4.0.0 / 2016-12-14 / branch "4-0-stable"
|
4
4
|
|
5
5
|
### Changed
|
6
6
|
* Render an opaque internal error by default for non-Liquid::Error (#835) [Dylan Thacker-Smith]
|
@@ -20,10 +20,13 @@
|
|
20
20
|
* Add concat filter to concatenate arrays (#429) [Diogo Beato]
|
21
21
|
* Ruby 1.9 support dropped (#491) [Justin Li]
|
22
22
|
* Liquid::Template.file_system's read_template_file method is no longer passed the context. (#441) [James Reid-Smith]
|
23
|
-
* Remove
|
23
|
+
* Remove `liquid_methods` (See https://github.com/Shopify/liquid/pull/568 for replacement)
|
24
24
|
* Liquid::Template.register_filter raises when the module overrides registered public methods as private or protected (#705) [Gaurav Chande]
|
25
25
|
|
26
26
|
### Fixed
|
27
|
+
|
28
|
+
* Fix variable names being detected as an operator when starting with contains (#788) [Michael Angell]
|
29
|
+
* Fix include tag used with strict_variables (#828) [QuickPay]
|
27
30
|
* Fix map filter when value is a Proc (#672) [Guillaume Malette]
|
28
31
|
* Fix truncate filter when value is not a string (#672) [Guillaume Malette]
|
29
32
|
* Fix behaviour of escape filter when input is nil (#665) [Tanel Jakobsoo]
|
data/README.md
CHANGED
@@ -42,6 +42,8 @@ Liquid is a template engine which was written with very specific requirements:
|
|
42
42
|
|
43
43
|
## How to use Liquid
|
44
44
|
|
45
|
+
Install Liquid by adding `gem 'liquid'` to your gemfile.
|
46
|
+
|
45
47
|
Liquid supports a very simple API based around the Liquid::Template class.
|
46
48
|
For standard use you can just pass it the content of a file and call render with a parameters hash.
|
47
49
|
|
data/lib/liquid/block.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
module Liquid
|
2
2
|
class Block < Tag
|
3
|
+
MAX_DEPTH = 100
|
4
|
+
|
3
5
|
def initialize(tag_name, markup, options)
|
4
6
|
super
|
5
7
|
@blank = true
|
@@ -24,12 +26,12 @@ module Liquid
|
|
24
26
|
end
|
25
27
|
|
26
28
|
def unknown_tag(tag, _params, _tokens)
|
27
|
-
|
28
|
-
when 'else'.freeze
|
29
|
+
if tag == 'else'.freeze
|
29
30
|
raise SyntaxError.new(parse_context.locale.t("errors.syntax.unexpected_else".freeze,
|
30
31
|
block_name: block_name))
|
31
|
-
|
32
|
+
elsif tag.start_with?('end'.freeze)
|
32
33
|
raise SyntaxError.new(parse_context.locale.t("errors.syntax.invalid_delimiter".freeze,
|
34
|
+
tag: tag,
|
33
35
|
block_name: block_name,
|
34
36
|
block_delimiter: block_delimiter))
|
35
37
|
else
|
@@ -48,17 +50,25 @@ module Liquid
|
|
48
50
|
protected
|
49
51
|
|
50
52
|
def parse_body(body, tokens)
|
51
|
-
|
52
|
-
|
53
|
+
if parse_context.depth >= MAX_DEPTH
|
54
|
+
raise StackLevelError, "Nesting too deep".freeze
|
55
|
+
end
|
56
|
+
parse_context.depth += 1
|
57
|
+
begin
|
58
|
+
body.parse(tokens, parse_context) do |end_tag_name, end_tag_params|
|
59
|
+
@blank &&= body.blank?
|
53
60
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
61
|
+
return false if end_tag_name == block_delimiter
|
62
|
+
unless end_tag_name
|
63
|
+
raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_never_closed".freeze, block_name: block_name))
|
64
|
+
end
|
58
65
|
|
59
|
-
|
60
|
-
|
61
|
-
|
66
|
+
# this tag is not registered with the system
|
67
|
+
# pass it to the current block for special handling or error reporting
|
68
|
+
unknown_tag(end_tag_name, end_tag_params, tokens)
|
69
|
+
end
|
70
|
+
ensure
|
71
|
+
parse_context.depth -= 1
|
62
72
|
end
|
63
73
|
|
64
74
|
true
|
data/lib/liquid/block_body.rb
CHANGED
@@ -2,6 +2,7 @@ module Liquid
|
|
2
2
|
class BlockBody
|
3
3
|
FullToken = /\A#{TagStart}#{WhitespaceControl}?\s*(\w+)\s*(.*?)#{WhitespaceControl}?#{TagEnd}\z/om
|
4
4
|
ContentOfVariable = /\A#{VariableStart}#{WhitespaceControl}?(.*?)#{WhitespaceControl}?#{VariableEnd}\z/om
|
5
|
+
WhitespaceOrNothing = /\A\s*\z/
|
5
6
|
TAGSTART = "{%".freeze
|
6
7
|
VARSTART = "{{".freeze
|
7
8
|
|
@@ -15,38 +16,35 @@ module Liquid
|
|
15
16
|
def parse(tokenizer, parse_context)
|
16
17
|
parse_context.line_number = tokenizer.line_number
|
17
18
|
while token = tokenizer.shift
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
markup = $2
|
25
|
-
# fetch the tag from registered blocks
|
26
|
-
if tag = registered_tags[tag_name]
|
27
|
-
new_tag = tag.parse(tag_name, markup, tokenizer, parse_context)
|
28
|
-
@blank &&= new_tag.blank?
|
29
|
-
@nodelist << new_tag
|
30
|
-
else
|
31
|
-
# end parsing if we reach an unknown tag and let the caller decide
|
32
|
-
# determine how to proceed
|
33
|
-
return yield tag_name, markup
|
34
|
-
end
|
35
|
-
else
|
36
|
-
raise_missing_tag_terminator(token, parse_context)
|
37
|
-
end
|
38
|
-
when token.start_with?(VARSTART)
|
39
|
-
whitespace_handler(token, parse_context)
|
40
|
-
@nodelist << create_variable(token, parse_context)
|
41
|
-
@blank = false
|
42
|
-
else
|
43
|
-
if parse_context.trim_whitespace
|
44
|
-
token.lstrip!
|
45
|
-
end
|
46
|
-
parse_context.trim_whitespace = false
|
47
|
-
@nodelist << token
|
48
|
-
@blank &&= !!(token =~ /\A\s*\z/)
|
19
|
+
next if token.empty?
|
20
|
+
case
|
21
|
+
when token.start_with?(TAGSTART)
|
22
|
+
whitespace_handler(token, parse_context)
|
23
|
+
unless token =~ FullToken
|
24
|
+
raise_missing_tag_terminator(token, parse_context)
|
49
25
|
end
|
26
|
+
tag_name = $1
|
27
|
+
markup = $2
|
28
|
+
# fetch the tag from registered blocks
|
29
|
+
unless tag = registered_tags[tag_name]
|
30
|
+
# end parsing if we reach an unknown tag and let the caller decide
|
31
|
+
# determine how to proceed
|
32
|
+
return yield tag_name, markup
|
33
|
+
end
|
34
|
+
new_tag = tag.parse(tag_name, markup, tokenizer, parse_context)
|
35
|
+
@blank &&= new_tag.blank?
|
36
|
+
@nodelist << new_tag
|
37
|
+
when token.start_with?(VARSTART)
|
38
|
+
whitespace_handler(token, parse_context)
|
39
|
+
@nodelist << create_variable(token, parse_context)
|
40
|
+
@blank = false
|
41
|
+
else
|
42
|
+
if parse_context.trim_whitespace
|
43
|
+
token.lstrip!
|
44
|
+
end
|
45
|
+
parse_context.trim_whitespace = false
|
46
|
+
@nodelist << token
|
47
|
+
@blank &&= !!(token =~ WhitespaceOrNothing)
|
50
48
|
end
|
51
49
|
parse_context.line_number = tokenizer.line_number
|
52
50
|
end
|
@@ -72,32 +70,27 @@ module Liquid
|
|
72
70
|
output = []
|
73
71
|
context.resource_limits.render_score += @nodelist.length
|
74
72
|
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
73
|
+
idx = 0
|
74
|
+
while node = @nodelist[idx]
|
75
|
+
case node
|
76
|
+
when String
|
77
|
+
check_resources(context, node)
|
78
|
+
output << node
|
79
|
+
when Variable
|
80
|
+
render_node_to_output(node, output, context)
|
81
|
+
when Block
|
82
|
+
render_node_to_output(node, output, context, node.blank?)
|
83
|
+
break if context.interrupt? # might have happened in a for-block
|
84
|
+
when Continue, Break
|
80
85
|
# If we get an Interrupt that means the block must stop processing. An
|
81
86
|
# Interrupt is any command that stops block execution such as {% break %}
|
82
87
|
# or {% continue %}
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
node_output = render_node(token, context)
|
89
|
-
|
90
|
-
unless token.is_a?(Block) && token.blank?
|
91
|
-
output << node_output
|
92
|
-
end
|
93
|
-
rescue MemoryError => e
|
94
|
-
raise e
|
95
|
-
rescue UndefinedVariable, UndefinedDropMethod, UndefinedFilter => e
|
96
|
-
context.handle_error(e, token.line_number, token.raw)
|
97
|
-
output << nil
|
98
|
-
rescue ::StandardError => e
|
99
|
-
output << context.handle_error(e, token.line_number, token.raw)
|
88
|
+
context.push_interrupt(node.interrupt)
|
89
|
+
break
|
90
|
+
else # Other non-Block tags
|
91
|
+
render_node_to_output(node, output, context)
|
100
92
|
end
|
93
|
+
idx += 1
|
101
94
|
end
|
102
95
|
|
103
96
|
output.join
|
@@ -105,15 +98,25 @@ module Liquid
|
|
105
98
|
|
106
99
|
private
|
107
100
|
|
108
|
-
def
|
109
|
-
node_output =
|
101
|
+
def render_node_to_output(node, output, context, skip_output = false)
|
102
|
+
node_output = node.render(context)
|
110
103
|
node_output = node_output.is_a?(Array) ? node_output.join : node_output.to_s
|
104
|
+
check_resources(context, node_output)
|
105
|
+
output << node_output unless skip_output
|
106
|
+
rescue MemoryError => e
|
107
|
+
raise e
|
108
|
+
rescue UndefinedVariable, UndefinedDropMethod, UndefinedFilter => e
|
109
|
+
context.handle_error(e, node.line_number)
|
110
|
+
output << nil
|
111
|
+
rescue ::StandardError => e
|
112
|
+
line_number = node.is_a?(String) ? nil : node.line_number
|
113
|
+
output << context.handle_error(e, line_number)
|
114
|
+
end
|
111
115
|
|
116
|
+
def check_resources(context, node_output)
|
112
117
|
context.resource_limits.render_length += node_output.length
|
113
|
-
|
114
|
-
|
115
|
-
end
|
116
|
-
node_output
|
118
|
+
return unless context.resource_limits.reached?
|
119
|
+
raise MemoryError.new("Memory limits exceeded".freeze)
|
117
120
|
end
|
118
121
|
|
119
122
|
def create_variable(token, parse_context)
|
data/lib/liquid/condition.rb
CHANGED
@@ -41,16 +41,22 @@ module Liquid
|
|
41
41
|
end
|
42
42
|
|
43
43
|
def evaluate(context = Context.new)
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
44
|
+
condition = self
|
45
|
+
result = nil
|
46
|
+
loop do
|
47
|
+
result = interpret_condition(condition.left, condition.right, condition.operator, context)
|
48
|
+
|
49
|
+
case condition.child_relation
|
50
|
+
when :or
|
51
|
+
break if result
|
52
|
+
when :and
|
53
|
+
break unless result
|
54
|
+
else
|
55
|
+
break
|
56
|
+
end
|
57
|
+
condition = condition.child_condition
|
53
58
|
end
|
59
|
+
result
|
54
60
|
end
|
55
61
|
|
56
62
|
def or(condition)
|
@@ -75,6 +81,10 @@ module Liquid
|
|
75
81
|
"#<Condition #{[@left, @operator, @right].compact.join(' '.freeze)}>"
|
76
82
|
end
|
77
83
|
|
84
|
+
protected
|
85
|
+
|
86
|
+
attr_reader :child_relation, :child_condition
|
87
|
+
|
78
88
|
private
|
79
89
|
|
80
90
|
def equal_variables(left, right)
|
@@ -110,7 +120,7 @@ module Liquid
|
|
110
120
|
|
111
121
|
if operation.respond_to?(:call)
|
112
122
|
operation.call(self, left, right)
|
113
|
-
elsif left.respond_to?(operation) && right.respond_to?(operation)
|
123
|
+
elsif left.respond_to?(operation) && right.respond_to?(operation) && !left.is_a?(Hash) && !right.is_a?(Hash)
|
114
124
|
begin
|
115
125
|
left.send(operation, right)
|
116
126
|
rescue ::ArgumentError => e
|
data/lib/liquid/context.rb
CHANGED
@@ -74,7 +74,7 @@ module Liquid
|
|
74
74
|
@interrupts.pop
|
75
75
|
end
|
76
76
|
|
77
|
-
def handle_error(e, line_number = nil
|
77
|
+
def handle_error(e, line_number = nil)
|
78
78
|
e = internal_error unless e.is_a?(Liquid::Error)
|
79
79
|
e.template_name ||= template_name
|
80
80
|
e.line_number ||= line_number
|
@@ -89,7 +89,7 @@ module Liquid
|
|
89
89
|
# Push new local scope on the stack. use <tt>Context#stack</tt> instead
|
90
90
|
def push(new_scope = {})
|
91
91
|
@scopes.unshift(new_scope)
|
92
|
-
raise StackLevelError, "Nesting too deep".freeze if @scopes.length >
|
92
|
+
raise StackLevelError, "Nesting too deep".freeze if @scopes.length > Block::MAX_DEPTH
|
93
93
|
end
|
94
94
|
|
95
95
|
# Merge a hash of variables in the current local scope
|
@@ -160,7 +160,7 @@ module Liquid
|
|
160
160
|
end
|
161
161
|
|
162
162
|
# Fetches an object starting at the local scope and then moving up the hierachy
|
163
|
-
def find_variable(key)
|
163
|
+
def find_variable(key, raise_on_not_found: true)
|
164
164
|
# This was changed from find() to find_index() because this is a very hot
|
165
165
|
# path and find_index() is optimized in MRI to reduce object allocation
|
166
166
|
index = @scopes.find_index { |s| s.key?(key) }
|
@@ -170,8 +170,10 @@ module Liquid
|
|
170
170
|
|
171
171
|
if scope.nil?
|
172
172
|
@environments.each do |e|
|
173
|
-
variable = lookup_and_evaluate(e, key)
|
174
|
-
|
173
|
+
variable = lookup_and_evaluate(e, key, raise_on_not_found: raise_on_not_found)
|
174
|
+
# When lookup returned a value OR there is no value but the lookup also did not raise
|
175
|
+
# then it is the value we are looking for.
|
176
|
+
if !variable.nil? || @strict_variables && raise_on_not_found
|
175
177
|
scope = e
|
176
178
|
break
|
177
179
|
end
|
@@ -179,7 +181,7 @@ module Liquid
|
|
179
181
|
end
|
180
182
|
|
181
183
|
scope ||= @environments.last || @scopes.last
|
182
|
-
variable ||= lookup_and_evaluate(scope, key)
|
184
|
+
variable ||= lookup_and_evaluate(scope, key, raise_on_not_found: raise_on_not_found)
|
183
185
|
|
184
186
|
variable = variable.to_liquid
|
185
187
|
variable.context = self if variable.respond_to?(:context=)
|
@@ -187,8 +189,8 @@ module Liquid
|
|
187
189
|
variable
|
188
190
|
end
|
189
191
|
|
190
|
-
def lookup_and_evaluate(obj, key)
|
191
|
-
if @strict_variables && obj.respond_to?(:key?) && !obj.key?(key)
|
192
|
+
def lookup_and_evaluate(obj, key, raise_on_not_found: true)
|
193
|
+
if @strict_variables && raise_on_not_found && obj.respond_to?(:key?) && !obj.key?(key)
|
192
194
|
raise Liquid::UndefinedVariable, "undefined variable #{key}"
|
193
195
|
end
|
194
196
|
|
data/lib/liquid/expression.rb
CHANGED
@@ -21,20 +21,24 @@ module Liquid
|
|
21
21
|
'empty'.freeze => MethodLiteral.new(:empty?, '').freeze
|
22
22
|
}
|
23
23
|
|
24
|
+
SINGLE_QUOTED_STRING = /\A'(.*)'\z/m
|
25
|
+
DOUBLE_QUOTED_STRING = /\A"(.*)"\z/m
|
26
|
+
INTEGERS_REGEX = /\A(-?\d+)\z/
|
27
|
+
FLOATS_REGEX = /\A(-?\d[\d\.]+)\z/
|
28
|
+
RANGES_REGEX = /\A\((\S+)\.\.(\S+)\)\z/
|
29
|
+
|
24
30
|
def self.parse(markup)
|
25
31
|
if LITERALS.key?(markup)
|
26
32
|
LITERALS[markup]
|
27
33
|
else
|
28
34
|
case markup
|
29
|
-
when
|
30
|
-
$1
|
31
|
-
when /\A"(.*)"\z/m # Double quoted strings
|
35
|
+
when SINGLE_QUOTED_STRING, DOUBLE_QUOTED_STRING
|
32
36
|
$1
|
33
|
-
when
|
37
|
+
when INTEGERS_REGEX
|
34
38
|
$1.to_i
|
35
|
-
when
|
39
|
+
when RANGES_REGEX
|
36
40
|
RangeLookup.parse($1, $2)
|
37
|
-
when
|
41
|
+
when FLOATS_REGEX
|
38
42
|
$1.to_f
|
39
43
|
else
|
40
44
|
VariableLookup.parse(markup)
|
data/lib/liquid/extensions.rb
CHANGED
data/lib/liquid/lexer.rb
CHANGED
@@ -18,17 +18,19 @@ module Liquid
|
|
18
18
|
DOUBLE_STRING_LITERAL = /"[^\"]*"/
|
19
19
|
NUMBER_LITERAL = /-?\d+(\.\d+)?/
|
20
20
|
DOTDOT = /\.\./
|
21
|
-
COMPARISON_OPERATOR = /==|!=|<>|<=?|>=?|contains/
|
21
|
+
COMPARISON_OPERATOR = /==|!=|<>|<=?|>=?|contains(?=\s)/
|
22
|
+
WHITESPACE_OR_NOTHING = /\s*/
|
22
23
|
|
23
24
|
def initialize(input)
|
24
|
-
@ss = StringScanner.new(input
|
25
|
+
@ss = StringScanner.new(input)
|
25
26
|
end
|
26
27
|
|
27
28
|
def tokenize
|
28
29
|
@output = []
|
29
30
|
|
30
31
|
until @ss.eos?
|
31
|
-
@ss.skip(
|
32
|
+
@ss.skip(WHITESPACE_OR_NOTHING)
|
33
|
+
break if @ss.eos?
|
32
34
|
tok = case
|
33
35
|
when t = @ss.scan(COMPARISON_OPERATOR) then [:comparison, t]
|
34
36
|
when t = @ss.scan(SINGLE_STRING_LITERAL) then [:string, t]
|
data/lib/liquid/locales/en.yml
CHANGED
@@ -14,7 +14,7 @@
|
|
14
14
|
if: "Syntax Error in tag 'if' - Valid syntax: if [expression]"
|
15
15
|
include: "Error in tag 'include' - Valid syntax: include '[template]' (with|for) [object|collection]"
|
16
16
|
unknown_tag: "Unknown tag '%{tag}'"
|
17
|
-
invalid_delimiter: "'
|
17
|
+
invalid_delimiter: "'%{tag}' is not a valid delimiter for %{block_name} tags. use %{block_delimiter}"
|
18
18
|
unexpected_else: "%{block_name} tag does not expect 'else' tag"
|
19
19
|
unexpected_outer_tag: "Unexpected outer '%{tag}' tag"
|
20
20
|
tag_termination: "Tag '%{token}' was not properly terminated with regexp: %{tag_end}"
|