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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA1:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 15b38ad40486ed2c0f9af07847fd0328c1ebcd76
|
|
4
|
+
data.tar.gz: 1bf5887d011005c058d0ed524d03e40926c96d59
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 176f0aa1184d56a180447e3353d90dc31a24b7c203c107dd80446520667b4f87a9f91eed07c7f0cfbd53c9f69572f2d818201d98d11b3ba1c21e4635f657a8ca
|
|
7
|
+
data.tar.gz: 81da9c0069bccfc02de2e5da4e78d1b8dddf2b382018c5e50d7c32a5a56c6df0b1f06187c78274b7e6f170d8d5d475e776f31f903663a861df636565c946e567
|
data/History.md
CHANGED
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
## 3.0.0 / not yet released / branch "master"
|
|
4
4
|
|
|
5
5
|
* ...
|
|
6
|
+
* Removed Block#end_tag. Instead, override parse with `super` followed by your code. See #446 [Dylan Thacker-Smith, dylanahsmith]
|
|
7
|
+
* Fixed condition with wrong data types, see #423 [Bogdan Gusiev]
|
|
8
|
+
* Add url_encode to standard filters, see #421 [Derrick Reimer, djreimer]
|
|
9
|
+
* Add uniq to standard filters [Florian Weingarten, fw42]
|
|
6
10
|
* Add exception_handler feature, see #397 and #254 [Bogdan Gusiev, bogdan and Florian Weingarten, fw42]
|
|
7
11
|
* Optimize variable parsing to avoid repeated regex evaluation during template rendering #383 [Jason Hiltz-Laforge, jasonhl]
|
|
8
12
|
* Optimize checking for block interrupts to reduce object allocation #380 [Jason Hiltz-Laforge, jasonhl]
|
data/README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
[](http://travis-ci.org/Shopify/liquid)
|
|
2
|
+
[](http://inch-ci.org/github/Shopify/liquid)
|
|
3
3
|
|
|
4
4
|
# Liquid template engine
|
|
5
5
|
|
data/lib/liquid.rb
CHANGED
|
@@ -52,18 +52,26 @@ require 'liquid/extensions'
|
|
|
52
52
|
require 'liquid/errors'
|
|
53
53
|
require 'liquid/interrupts'
|
|
54
54
|
require 'liquid/strainer'
|
|
55
|
+
require 'liquid/expression'
|
|
55
56
|
require 'liquid/context'
|
|
57
|
+
require 'liquid/parser_switching'
|
|
56
58
|
require 'liquid/tag'
|
|
57
59
|
require 'liquid/block'
|
|
58
60
|
require 'liquid/document'
|
|
59
61
|
require 'liquid/variable'
|
|
62
|
+
require 'liquid/variable_lookup'
|
|
63
|
+
require 'liquid/range_lookup'
|
|
60
64
|
require 'liquid/file_system'
|
|
61
65
|
require 'liquid/template'
|
|
62
66
|
require 'liquid/standardfilters'
|
|
63
67
|
require 'liquid/condition'
|
|
64
68
|
require 'liquid/module_ex'
|
|
65
69
|
require 'liquid/utils'
|
|
70
|
+
require 'liquid/token'
|
|
66
71
|
|
|
67
72
|
# Load all the tags of the standard library
|
|
68
73
|
#
|
|
69
74
|
Dir[File.dirname(__FILE__) + '/liquid/tags/*.rb'].each { |f| require f }
|
|
75
|
+
|
|
76
|
+
require 'liquid/profiler'
|
|
77
|
+
require 'liquid/profiler/hooks'
|
data/lib/liquid/block.rb
CHANGED
|
@@ -14,45 +14,45 @@ module Liquid
|
|
|
14
14
|
@nodelist ||= []
|
|
15
15
|
@nodelist.clear
|
|
16
16
|
|
|
17
|
-
# All child tags of the current block.
|
|
18
|
-
@children = []
|
|
19
|
-
|
|
20
17
|
while token = tokens.shift
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
18
|
+
begin
|
|
19
|
+
unless token.empty?
|
|
20
|
+
case
|
|
21
|
+
when token.start_with?(TAGSTART)
|
|
22
|
+
if token =~ FullToken
|
|
23
|
+
|
|
24
|
+
# if we found the proper block delimiter just end parsing here and let the outer block
|
|
25
|
+
# proceed
|
|
26
|
+
return if block_delimiter == $1
|
|
27
|
+
|
|
28
|
+
# fetch the tag from registered blocks
|
|
29
|
+
if tag = Template.tags[$1]
|
|
30
|
+
markup = token.is_a?(Token) ? token.child($2) : $2
|
|
31
|
+
new_tag = tag.parse($1, markup, tokens, @options)
|
|
32
|
+
new_tag.line_number = token.line_number if token.is_a?(Token)
|
|
33
|
+
@blank &&= new_tag.blank?
|
|
34
|
+
@nodelist << new_tag
|
|
35
|
+
else
|
|
36
|
+
# this tag is not registered with the system
|
|
37
|
+
# pass it to the current block for special handling or error reporting
|
|
38
|
+
unknown_tag($1, $2, tokens)
|
|
39
|
+
end
|
|
39
40
|
else
|
|
40
|
-
|
|
41
|
-
# pass it to the current block for special handling or error reporting
|
|
42
|
-
unknown_tag($1, $2, tokens)
|
|
41
|
+
raise SyntaxError.new(options[:locale].t("errors.syntax.tag_termination".freeze, :token => token, :tag_end => TagEnd.inspect))
|
|
43
42
|
end
|
|
43
|
+
when token.start_with?(VARSTART)
|
|
44
|
+
new_var = create_variable(token)
|
|
45
|
+
new_var.line_number = token.line_number if token.is_a?(Token)
|
|
46
|
+
@nodelist << new_var
|
|
47
|
+
@blank = false
|
|
44
48
|
else
|
|
45
|
-
|
|
49
|
+
@nodelist << token
|
|
50
|
+
@blank &&= (token =~ /\A\s*\z/)
|
|
46
51
|
end
|
|
47
|
-
when token.start_with?(VARSTART)
|
|
48
|
-
new_var = create_variable(token)
|
|
49
|
-
@nodelist << new_var
|
|
50
|
-
@children << new_var
|
|
51
|
-
@blank = false
|
|
52
|
-
else
|
|
53
|
-
@nodelist << token
|
|
54
|
-
@blank &&= (token =~ /\A\s*\z/)
|
|
55
52
|
end
|
|
53
|
+
rescue SyntaxError => e
|
|
54
|
+
e.set_line_number_from_token(token)
|
|
55
|
+
raise
|
|
56
56
|
end
|
|
57
57
|
end
|
|
58
58
|
|
|
@@ -67,16 +67,13 @@ module Liquid
|
|
|
67
67
|
all_warnings = []
|
|
68
68
|
all_warnings.concat(@warnings) if @warnings
|
|
69
69
|
|
|
70
|
-
(
|
|
71
|
-
all_warnings.concat(node.warnings || [])
|
|
70
|
+
(nodelist || []).each do |node|
|
|
71
|
+
all_warnings.concat(node.warnings || []) if node.respond_to?(:warnings)
|
|
72
72
|
end
|
|
73
73
|
|
|
74
74
|
all_warnings
|
|
75
75
|
end
|
|
76
76
|
|
|
77
|
-
def end_tag
|
|
78
|
-
end
|
|
79
|
-
|
|
80
77
|
def unknown_tag(tag, params, tokens)
|
|
81
78
|
case tag
|
|
82
79
|
when 'else'.freeze
|
|
@@ -101,7 +98,8 @@ module Liquid
|
|
|
101
98
|
|
|
102
99
|
def create_variable(token)
|
|
103
100
|
token.scan(ContentOfVariable) do |content|
|
|
104
|
-
|
|
101
|
+
markup = token.is_a?(Token) ? token.child(content.first) : content.first
|
|
102
|
+
return Variable.new(markup, @options)
|
|
105
103
|
end
|
|
106
104
|
raise SyntaxError.new(options[:locale].t("errors.syntax.variable_termination".freeze, :token => token, :tag_end => VariableEnd.inspect))
|
|
107
105
|
end
|
|
@@ -134,23 +132,29 @@ module Liquid
|
|
|
134
132
|
break
|
|
135
133
|
end
|
|
136
134
|
|
|
137
|
-
token_output = (token
|
|
138
|
-
|
|
139
|
-
if context.resource_limits_reached?
|
|
140
|
-
context.resource_limits[:reached] = true
|
|
141
|
-
raise MemoryError.new("Memory limits exceeded".freeze)
|
|
142
|
-
end
|
|
135
|
+
token_output = render_token(token, context)
|
|
136
|
+
|
|
143
137
|
unless token.is_a?(Block) && token.blank?
|
|
144
138
|
output << token_output
|
|
145
139
|
end
|
|
146
140
|
rescue MemoryError => e
|
|
147
141
|
raise e
|
|
148
142
|
rescue ::StandardError => e
|
|
149
|
-
output << (context.handle_error(e))
|
|
143
|
+
output << (context.handle_error(e, token))
|
|
150
144
|
end
|
|
151
145
|
end
|
|
152
146
|
|
|
153
147
|
output.join
|
|
154
148
|
end
|
|
149
|
+
|
|
150
|
+
def render_token(token, context)
|
|
151
|
+
token_output = (token.respond_to?(:render) ? token.render(context) : token)
|
|
152
|
+
context.increment_used_resources(:render_length_current, token_output)
|
|
153
|
+
if context.resource_limits_reached?
|
|
154
|
+
context.resource_limits[:reached] = true
|
|
155
|
+
raise MemoryError.new("Memory limits exceeded".freeze)
|
|
156
|
+
end
|
|
157
|
+
token_output
|
|
158
|
+
end
|
|
155
159
|
end
|
|
156
160
|
end
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
module Liquid
|
|
2
|
+
class BlockBody
|
|
3
|
+
FullToken = /\A#{TagStart}\s*(\w+)\s*(.*)?#{TagEnd}\z/om
|
|
4
|
+
ContentOfVariable = /\A#{VariableStart}(.*)#{VariableEnd}\z/om
|
|
5
|
+
TAGSTART = "{%".freeze
|
|
6
|
+
VARSTART = "{{".freeze
|
|
7
|
+
|
|
8
|
+
attr_reader :nodelist
|
|
9
|
+
|
|
10
|
+
def initialize
|
|
11
|
+
@nodelist = []
|
|
12
|
+
@blank = true
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def parse(tokens, options)
|
|
16
|
+
while token = tokens.shift
|
|
17
|
+
begin
|
|
18
|
+
unless token.empty?
|
|
19
|
+
case
|
|
20
|
+
when token.start_with?(TAGSTART)
|
|
21
|
+
if token =~ FullToken
|
|
22
|
+
tag_name = $1
|
|
23
|
+
markup = $2
|
|
24
|
+
# fetch the tag from registered blocks
|
|
25
|
+
if tag = Template.tags[tag_name]
|
|
26
|
+
markup = token.child(markup) if token.is_a?(Token)
|
|
27
|
+
new_tag = tag.parse(tag_name, markup, tokens, options)
|
|
28
|
+
new_tag.line_number = token.line_number if token.is_a?(Token)
|
|
29
|
+
@blank &&= new_tag.blank?
|
|
30
|
+
@nodelist << new_tag
|
|
31
|
+
else
|
|
32
|
+
# end parsing if we reach an unknown tag and let the caller decide
|
|
33
|
+
# determine how to proceed
|
|
34
|
+
return yield tag_name, markup
|
|
35
|
+
end
|
|
36
|
+
else
|
|
37
|
+
raise SyntaxError.new(options[:locale].t("errors.syntax.tag_termination".freeze, :token => token, :tag_end => TagEnd.inspect))
|
|
38
|
+
end
|
|
39
|
+
when token.start_with?(VARSTART)
|
|
40
|
+
new_var = create_variable(token, options)
|
|
41
|
+
new_var.line_number = token.line_number if token.is_a?(Token)
|
|
42
|
+
@nodelist << new_var
|
|
43
|
+
@blank = false
|
|
44
|
+
else
|
|
45
|
+
@nodelist << token
|
|
46
|
+
@blank &&= !!(token =~ /\A\s*\z/)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
rescue SyntaxError => e
|
|
50
|
+
e.set_line_number_from_token(token)
|
|
51
|
+
raise
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
yield nil, nil
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def blank?
|
|
59
|
+
@blank
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def warnings
|
|
63
|
+
all_warnings = []
|
|
64
|
+
nodelist.each do |node|
|
|
65
|
+
all_warnings.concat(node.warnings) if node.respond_to?(:warnings) && node.warnings
|
|
66
|
+
end
|
|
67
|
+
all_warnings
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def render(context)
|
|
71
|
+
output = []
|
|
72
|
+
context.resource_limits[:render_length_current] = 0
|
|
73
|
+
context.resource_limits[:render_score_current] += @nodelist.length
|
|
74
|
+
|
|
75
|
+
@nodelist.each do |token|
|
|
76
|
+
# Break out if we have any unhanded interrupts.
|
|
77
|
+
break if context.has_interrupt?
|
|
78
|
+
|
|
79
|
+
begin
|
|
80
|
+
# If we get an Interrupt that means the block must stop processing. An
|
|
81
|
+
# Interrupt is any command that stops block execution such as {% break %}
|
|
82
|
+
# or {% continue %}
|
|
83
|
+
if token.is_a?(Continue) or token.is_a?(Break)
|
|
84
|
+
context.push_interrupt(token.interrupt)
|
|
85
|
+
break
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
token_output = render_token(token, context)
|
|
89
|
+
|
|
90
|
+
unless token.is_a?(Block) && token.blank?
|
|
91
|
+
output << token_output
|
|
92
|
+
end
|
|
93
|
+
rescue MemoryError => e
|
|
94
|
+
raise e
|
|
95
|
+
rescue ::StandardError => e
|
|
96
|
+
output << context.handle_error(e, token)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
output.join
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
private
|
|
104
|
+
|
|
105
|
+
def render_token(token, context)
|
|
106
|
+
token_output = (token.respond_to?(:render) ? token.render(context) : token)
|
|
107
|
+
context.increment_used_resources(:render_length_current, token_output)
|
|
108
|
+
if context.resource_limits_reached?
|
|
109
|
+
context.resource_limits[:reached] = true
|
|
110
|
+
raise MemoryError.new("Memory limits exceeded".freeze)
|
|
111
|
+
end
|
|
112
|
+
token_output
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def create_variable(token, options)
|
|
116
|
+
token.scan(ContentOfVariable) do |content|
|
|
117
|
+
markup = token.is_a?(Token) ? token.child(content.first) : content.first
|
|
118
|
+
return Variable.new(markup, options)
|
|
119
|
+
end
|
|
120
|
+
raise SyntaxError.new(options[:locale].t("errors.syntax.variable_termination".freeze, :token => token, :tag_end => VariableEnd.inspect))
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
data/lib/liquid/condition.rb
CHANGED
|
@@ -15,7 +15,9 @@ module Liquid
|
|
|
15
15
|
'>'.freeze => :>,
|
|
16
16
|
'>='.freeze => :>=,
|
|
17
17
|
'<='.freeze => :<=,
|
|
18
|
-
'contains'.freeze => lambda { |cond, left, right|
|
|
18
|
+
'contains'.freeze => lambda { |cond, left, right|
|
|
19
|
+
left && right && left.respond_to?(:include?) ? left.include?(right) : false
|
|
20
|
+
}
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
def self.operators
|
|
@@ -26,7 +28,9 @@ module Liquid
|
|
|
26
28
|
attr_accessor :left, :operator, :right
|
|
27
29
|
|
|
28
30
|
def initialize(left = nil, operator = nil, right = nil)
|
|
29
|
-
@left
|
|
31
|
+
@left = left
|
|
32
|
+
@operator = operator
|
|
33
|
+
@right = right
|
|
30
34
|
@child_relation = nil
|
|
31
35
|
@child_condition = nil
|
|
32
36
|
end
|
|
@@ -45,11 +49,13 @@ module Liquid
|
|
|
45
49
|
end
|
|
46
50
|
|
|
47
51
|
def or(condition)
|
|
48
|
-
@child_relation
|
|
52
|
+
@child_relation = :or
|
|
53
|
+
@child_condition = condition
|
|
49
54
|
end
|
|
50
55
|
|
|
51
56
|
def and(condition)
|
|
52
|
-
@child_relation
|
|
57
|
+
@child_relation = :and
|
|
58
|
+
@child_condition = condition
|
|
53
59
|
end
|
|
54
60
|
|
|
55
61
|
def attach(attachment)
|
|
@@ -92,7 +98,8 @@ module Liquid
|
|
|
92
98
|
# return this as the result.
|
|
93
99
|
return context[left] if op == nil
|
|
94
100
|
|
|
95
|
-
left
|
|
101
|
+
left = context[left]
|
|
102
|
+
right = context[right]
|
|
96
103
|
|
|
97
104
|
operation = self.class.operators[op] || raise(Liquid::ArgumentError.new("Unknown operator #{op}"))
|
|
98
105
|
|
data/lib/liquid/context.rb
CHANGED
|
@@ -16,23 +16,25 @@ module Liquid
|
|
|
16
16
|
attr_reader :scopes, :errors, :registers, :environments, :resource_limits
|
|
17
17
|
attr_accessor :exception_handler
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
@
|
|
23
|
-
@
|
|
24
|
-
@
|
|
25
|
-
@
|
|
26
|
-
@resource_limits
|
|
19
|
+
def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = nil)
|
|
20
|
+
@environments = [environments].flatten
|
|
21
|
+
@scopes = [(outer_scope || {})]
|
|
22
|
+
@registers = registers
|
|
23
|
+
@errors = []
|
|
24
|
+
@resource_limits = resource_limits || Template.default_resource_limits.dup
|
|
25
|
+
@resource_limits[:render_score_current] = 0
|
|
26
|
+
@resource_limits[:assign_score_current] = 0
|
|
27
|
+
@parsed_expression = Hash.new{ |cache, markup| cache[markup] = Expression.parse(markup) }
|
|
27
28
|
squash_instance_assigns_with_environments
|
|
28
29
|
|
|
30
|
+
@this_stack_used = false
|
|
31
|
+
|
|
29
32
|
if rethrow_errors
|
|
30
33
|
self.exception_handler = ->(e) { true }
|
|
31
34
|
end
|
|
32
35
|
|
|
33
36
|
@interrupts = []
|
|
34
37
|
@filters = []
|
|
35
|
-
@parsed_variables = Hash.new{ |cache, markup| cache[markup] = variable_parse(markup) }
|
|
36
38
|
end
|
|
37
39
|
|
|
38
40
|
def increment_used_resources(key, obj)
|
|
@@ -91,21 +93,19 @@ module Liquid
|
|
|
91
93
|
@interrupts.pop
|
|
92
94
|
end
|
|
93
95
|
|
|
94
|
-
def handle_error(e)
|
|
95
|
-
errors.push(e)
|
|
96
|
-
|
|
97
|
-
raise if exception_handler && exception_handler.call(e)
|
|
98
96
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
else
|
|
103
|
-
"Liquid error: #{e.message}"
|
|
97
|
+
def handle_error(e, token=nil)
|
|
98
|
+
if e.is_a?(Liquid::Error)
|
|
99
|
+
e.set_line_number_from_token(token)
|
|
104
100
|
end
|
|
101
|
+
|
|
102
|
+
errors.push(e)
|
|
103
|
+
raise if exception_handler && exception_handler.call(e)
|
|
104
|
+
Liquid::Error.render(e)
|
|
105
105
|
end
|
|
106
106
|
|
|
107
107
|
def invoke(method, *args)
|
|
108
|
-
strainer.invoke(method, *args)
|
|
108
|
+
strainer.invoke(method, *args).to_liquid
|
|
109
109
|
end
|
|
110
110
|
|
|
111
111
|
# Push new local scope on the stack. use <tt>Context#stack</tt> instead
|
|
@@ -133,11 +133,19 @@ module Liquid
|
|
|
133
133
|
# end
|
|
134
134
|
#
|
|
135
135
|
# context['var] #=> nil
|
|
136
|
-
def stack(new_scope=
|
|
137
|
-
|
|
136
|
+
def stack(new_scope=nil)
|
|
137
|
+
old_stack_used = @this_stack_used
|
|
138
|
+
if new_scope
|
|
139
|
+
push(new_scope)
|
|
140
|
+
@this_stack_used = true
|
|
141
|
+
else
|
|
142
|
+
@this_stack_used = false
|
|
143
|
+
end
|
|
144
|
+
|
|
138
145
|
yield
|
|
139
146
|
ensure
|
|
140
|
-
pop
|
|
147
|
+
pop if @this_stack_used
|
|
148
|
+
@this_stack_used = old_stack_used
|
|
141
149
|
end
|
|
142
150
|
|
|
143
151
|
def clear_instance_assigns
|
|
@@ -146,152 +154,71 @@ module Liquid
|
|
|
146
154
|
|
|
147
155
|
# Only allow String, Numeric, Hash, Array, Proc, Boolean or <tt>Liquid::Drop</tt>
|
|
148
156
|
def []=(key, value)
|
|
157
|
+
unless @this_stack_used
|
|
158
|
+
@this_stack_used = true
|
|
159
|
+
push({})
|
|
160
|
+
end
|
|
149
161
|
@scopes[0][key] = value
|
|
150
162
|
end
|
|
151
163
|
|
|
152
|
-
|
|
153
|
-
|
|
164
|
+
# Look up variable, either resolve directly after considering the name. We can directly handle
|
|
165
|
+
# Strings, digits, floats and booleans (true,false).
|
|
166
|
+
# If no match is made we lookup the variable in the current scope and
|
|
167
|
+
# later move up to the parent blocks to see if we can resolve the variable somewhere up the tree.
|
|
168
|
+
# Some special keywords return symbols. Those symbols are to be called on the rhs object in expressions
|
|
169
|
+
#
|
|
170
|
+
# Example:
|
|
171
|
+
# products == empty #=> products.empty?
|
|
172
|
+
def [](expression)
|
|
173
|
+
evaluate(@parsed_expression[expression])
|
|
154
174
|
end
|
|
155
175
|
|
|
156
176
|
def has_key?(key)
|
|
157
|
-
|
|
177
|
+
self[key] != nil
|
|
158
178
|
end
|
|
159
179
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
'true'.freeze => true,
|
|
164
|
-
'false'.freeze => false,
|
|
165
|
-
'blank'.freeze => :blank?,
|
|
166
|
-
'empty'.freeze => :empty?
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
# Look up variable, either resolve directly after considering the name. We can directly handle
|
|
170
|
-
# Strings, digits, floats and booleans (true,false).
|
|
171
|
-
# If no match is made we lookup the variable in the current scope and
|
|
172
|
-
# later move up to the parent blocks to see if we can resolve the variable somewhere up the tree.
|
|
173
|
-
# Some special keywords return symbols. Those symbols are to be called on the rhs object in expressions
|
|
174
|
-
#
|
|
175
|
-
# Example:
|
|
176
|
-
# products == empty #=> products.empty?
|
|
177
|
-
def resolve(key)
|
|
178
|
-
if LITERALS.key?(key)
|
|
179
|
-
LITERALS[key]
|
|
180
|
-
else
|
|
181
|
-
case key
|
|
182
|
-
when /\A'(.*)'\z/m # Single quoted strings
|
|
183
|
-
$1
|
|
184
|
-
when /\A"(.*)"\z/m # Double quoted strings
|
|
185
|
-
$1
|
|
186
|
-
when /\A(-?\d+)\z/ # Integer and floats
|
|
187
|
-
$1.to_i
|
|
188
|
-
when /\A\((\S+)\.\.(\S+)\)\z/ # Ranges
|
|
189
|
-
(resolve($1).to_i..resolve($2).to_i)
|
|
190
|
-
when /\A(-?\d[\d\.]+)\z/ # Floats
|
|
191
|
-
$1.to_f
|
|
192
|
-
else
|
|
193
|
-
variable(key)
|
|
194
|
-
end
|
|
195
|
-
end
|
|
196
|
-
end
|
|
180
|
+
def evaluate(object)
|
|
181
|
+
object.respond_to?(:evaluate) ? object.evaluate(self) : object
|
|
182
|
+
end
|
|
197
183
|
|
|
198
|
-
|
|
199
|
-
|
|
184
|
+
# Fetches an object starting at the local scope and then moving up the hierachy
|
|
185
|
+
def find_variable(key)
|
|
200
186
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
187
|
+
# This was changed from find() to find_index() because this is a very hot
|
|
188
|
+
# path and find_index() is optimized in MRI to reduce object allocation
|
|
189
|
+
index = @scopes.find_index { |s| s.has_key?(key) }
|
|
190
|
+
scope = @scopes[index] if index
|
|
205
191
|
|
|
206
|
-
|
|
192
|
+
variable = nil
|
|
207
193
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
end
|
|
194
|
+
if scope.nil?
|
|
195
|
+
@environments.each do |e|
|
|
196
|
+
variable = lookup_and_evaluate(e, key)
|
|
197
|
+
unless variable.nil?
|
|
198
|
+
scope = e
|
|
199
|
+
break
|
|
215
200
|
end
|
|
216
201
|
end
|
|
217
|
-
|
|
218
|
-
scope ||= @environments.last || @scopes.last
|
|
219
|
-
variable ||= lookup_and_evaluate(scope, key)
|
|
220
|
-
|
|
221
|
-
variable = variable.to_liquid
|
|
222
|
-
variable.context = self if variable.respond_to?(:context=)
|
|
223
|
-
|
|
224
|
-
return variable
|
|
225
|
-
end
|
|
226
|
-
|
|
227
|
-
def variable_parse(markup)
|
|
228
|
-
parts = markup.scan(VariableParser)
|
|
229
|
-
needs_resolution = false
|
|
230
|
-
if parts.first =~ SQUARE_BRACKETED
|
|
231
|
-
needs_resolution = true
|
|
232
|
-
parts[0] = $1
|
|
233
|
-
end
|
|
234
|
-
{:first => parts.shift, :needs_resolution => needs_resolution, :rest => parts}
|
|
235
202
|
end
|
|
236
203
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
# Example
|
|
240
|
-
# @context['hash'] = {"name" => 'tobi'}
|
|
241
|
-
# assert_equal 'tobi', @context['hash.name']
|
|
242
|
-
# assert_equal 'tobi', @context['hash["name"]']
|
|
243
|
-
def variable(markup)
|
|
244
|
-
parts = @parsed_variables[markup]
|
|
245
|
-
|
|
246
|
-
first_part = parts[:first]
|
|
247
|
-
if parts[:needs_resolution]
|
|
248
|
-
first_part = resolve(parts[:first])
|
|
249
|
-
end
|
|
250
|
-
|
|
251
|
-
if object = find_variable(first_part)
|
|
252
|
-
|
|
253
|
-
parts[:rest].each do |part|
|
|
254
|
-
part = resolve($1) if part_resolved = (part =~ SQUARE_BRACKETED)
|
|
255
|
-
|
|
256
|
-
# If object is a hash- or array-like object we look for the
|
|
257
|
-
# presence of the key and if its available we return it
|
|
258
|
-
if object.respond_to?(:[]) and
|
|
259
|
-
((object.respond_to?(:has_key?) and object.has_key?(part)) or
|
|
260
|
-
(object.respond_to?(:fetch) and part.is_a?(Integer)))
|
|
261
|
-
|
|
262
|
-
# if its a proc we will replace the entry with the proc
|
|
263
|
-
res = lookup_and_evaluate(object, part)
|
|
264
|
-
object = res.to_liquid
|
|
265
|
-
|
|
266
|
-
# Some special cases. If the part wasn't in square brackets and
|
|
267
|
-
# no key with the same name was found we interpret following calls
|
|
268
|
-
# as commands and call them on the current object
|
|
269
|
-
elsif !part_resolved and object.respond_to?(part) and ['size'.freeze, 'first'.freeze, 'last'.freeze].include?(part)
|
|
204
|
+
scope ||= @environments.last || @scopes.last
|
|
205
|
+
variable ||= lookup_and_evaluate(scope, key)
|
|
270
206
|
|
|
271
|
-
|
|
207
|
+
variable = variable.to_liquid
|
|
208
|
+
variable.context = self if variable.respond_to?(:context=)
|
|
272
209
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
else
|
|
276
|
-
return nil
|
|
277
|
-
end
|
|
278
|
-
|
|
279
|
-
# If we are dealing with a drop here we have to
|
|
280
|
-
object.context = self if object.respond_to?(:context=)
|
|
281
|
-
end
|
|
282
|
-
end
|
|
283
|
-
|
|
284
|
-
object
|
|
285
|
-
end # variable
|
|
210
|
+
return variable
|
|
211
|
+
end
|
|
286
212
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
213
|
+
def lookup_and_evaluate(obj, key)
|
|
214
|
+
if (value = obj[key]).is_a?(Proc) && obj.respond_to?(:[]=)
|
|
215
|
+
obj[key] = (value.arity == 0) ? value.call : value.call(self)
|
|
216
|
+
else
|
|
217
|
+
value
|
|
218
|
+
end
|
|
219
|
+
end
|
|
294
220
|
|
|
221
|
+
private
|
|
295
222
|
def squash_instance_assigns_with_environments
|
|
296
223
|
@scopes.last.each_key do |k|
|
|
297
224
|
@environments.each do |env|
|