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
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
|
-
[![Build Status](https://
|
2
|
-
[![Inline docs](http://inch-ci.org/github/Shopify/liquid.
|
1
|
+
[![Build Status](https://api.travis-ci.org/Shopify/liquid.svg?branch=master)](http://travis-ci.org/Shopify/liquid)
|
2
|
+
[![Inline docs](http://inch-ci.org/github/Shopify/liquid.svg?branch=master)](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|
|