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.
Files changed (73) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +4 -0
  3. data/README.md +2 -2
  4. data/lib/liquid.rb +8 -0
  5. data/lib/liquid/block.rb +50 -46
  6. data/lib/liquid/block_body.rb +123 -0
  7. data/lib/liquid/condition.rb +12 -5
  8. data/lib/liquid/context.rb +75 -148
  9. data/lib/liquid/errors.rb +50 -2
  10. data/lib/liquid/expression.rb +33 -0
  11. data/lib/liquid/parser_switching.rb +31 -0
  12. data/lib/liquid/profiler.rb +159 -0
  13. data/lib/liquid/profiler/hooks.rb +23 -0
  14. data/lib/liquid/range_lookup.rb +22 -0
  15. data/lib/liquid/standardfilters.rb +29 -4
  16. data/lib/liquid/tag.rb +6 -25
  17. data/lib/liquid/tags/assign.rb +2 -1
  18. data/lib/liquid/tags/case.rb +1 -1
  19. data/lib/liquid/tags/if.rb +5 -5
  20. data/lib/liquid/tags/ifchanged.rb +1 -1
  21. data/lib/liquid/tags/include.rb +11 -1
  22. data/lib/liquid/tags/raw.rb +1 -4
  23. data/lib/liquid/tags/table_row.rb +1 -1
  24. data/lib/liquid/template.rb +55 -4
  25. data/lib/liquid/token.rb +18 -0
  26. data/lib/liquid/variable.rb +68 -41
  27. data/lib/liquid/variable_lookup.rb +78 -0
  28. data/lib/liquid/version.rb +1 -1
  29. data/test/integration/assign_test.rb +12 -1
  30. data/test/integration/blank_test.rb +1 -1
  31. data/test/integration/capture_test.rb +1 -1
  32. data/test/integration/context_test.rb +10 -11
  33. data/test/integration/drop_test.rb +29 -3
  34. data/test/integration/error_handling_test.rb +138 -41
  35. data/test/integration/filter_test.rb +7 -7
  36. data/test/integration/hash_ordering_test.rb +6 -8
  37. data/test/integration/output_test.rb +1 -1
  38. data/test/integration/parsing_quirks_test.rb +40 -18
  39. data/test/integration/render_profiling_test.rb +154 -0
  40. data/test/integration/security_test.rb +1 -1
  41. data/test/integration/standard_filter_test.rb +47 -1
  42. data/test/integration/tags/break_tag_test.rb +1 -1
  43. data/test/integration/tags/continue_tag_test.rb +1 -1
  44. data/test/integration/tags/for_tag_test.rb +2 -2
  45. data/test/integration/tags/if_else_tag_test.rb +23 -20
  46. data/test/integration/tags/include_tag_test.rb +24 -2
  47. data/test/integration/tags/increment_tag_test.rb +1 -1
  48. data/test/integration/tags/raw_tag_test.rb +1 -1
  49. data/test/integration/tags/standard_tag_test.rb +4 -4
  50. data/test/integration/tags/statements_test.rb +1 -1
  51. data/test/integration/tags/table_row_test.rb +1 -1
  52. data/test/integration/tags/unless_else_tag_test.rb +1 -1
  53. data/test/integration/template_test.rb +16 -4
  54. data/test/integration/variable_test.rb +11 -1
  55. data/test/test_helper.rb +59 -31
  56. data/test/unit/block_unit_test.rb +2 -5
  57. data/test/unit/condition_unit_test.rb +5 -1
  58. data/test/unit/context_unit_test.rb +13 -7
  59. data/test/unit/file_system_unit_test.rb +5 -5
  60. data/test/unit/i18n_unit_test.rb +3 -3
  61. data/test/unit/lexer_unit_test.rb +1 -1
  62. data/test/unit/module_ex_unit_test.rb +1 -1
  63. data/test/unit/parser_unit_test.rb +1 -1
  64. data/test/unit/regexp_unit_test.rb +1 -1
  65. data/test/unit/strainer_unit_test.rb +3 -2
  66. data/test/unit/tag_unit_test.rb +6 -1
  67. data/test/unit/tags/case_tag_unit_test.rb +1 -1
  68. data/test/unit/tags/for_tag_unit_test.rb +1 -1
  69. data/test/unit/tags/if_tag_unit_test.rb +1 -1
  70. data/test/unit/template_unit_test.rb +1 -1
  71. data/test/unit/tokenizer_unit_test.rb +10 -1
  72. data/test/unit/variable_unit_test.rb +49 -46
  73. metadata +71 -47
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: f98aa57f8713db177b94a4cd217b6c5f40234bc5
4
- data.tar.gz: 50a9905b550fcedf21315f4764360f104d0964bb
3
+ metadata.gz: 15b38ad40486ed2c0f9af07847fd0328c1ebcd76
4
+ data.tar.gz: 1bf5887d011005c058d0ed524d03e40926c96d59
5
5
  SHA512:
6
- metadata.gz: 7c9c6d8d006ef94568651f21dba6d4619cef50c575510e74159602140263a10f20148b4c278c17aa77221690e7dedb547aedf4af5fc866b1c3ab4644243b8d68
7
- data.tar.gz: ff19880456ff443065ed50371ad4223fedcf350494564ba69e51c9c2c4e06281ffc54773e9f0cbae60fbee95a60be4514f13924b0d831db43a41fcabadab1a8c
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://secure.travis-ci.org/Shopify/liquid.png?branch=master)](http://travis-ci.org/Shopify/liquid)
2
- [![Inline docs](http://inch-ci.org/github/Shopify/liquid.png)](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
 
@@ -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'
@@ -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
- unless token.empty?
22
- case
23
- when token.start_with?(TAGSTART)
24
- if token =~ FullToken
25
-
26
- # if we found the proper block delimiter just end parsing here and let the outer block
27
- # proceed
28
- if block_delimiter == $1
29
- end_tag
30
- return
31
- end
32
-
33
- # fetch the tag from registered blocks
34
- if tag = Template.tags[$1]
35
- new_tag = tag.parse($1, $2, tokens, @options)
36
- @blank &&= new_tag.blank?
37
- @nodelist << new_tag
38
- @children << new_tag
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
- # this tag is not registered with the system
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
- raise SyntaxError.new(options[:locale].t("errors.syntax.tag_termination".freeze, :token => token, :tag_end => TagEnd.inspect))
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
- (@children || []).each do |node|
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
- return Variable.new(content.first, @options)
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.respond_to?(:render) ? token.render(context) : token)
138
- context.increment_used_resources(:render_length_current, token_output)
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
@@ -15,7 +15,9 @@ module Liquid
15
15
  '>'.freeze => :>,
16
16
  '>='.freeze => :>=,
17
17
  '<='.freeze => :<=,
18
- 'contains'.freeze => lambda { |cond, left, right| left && right ? left.include?(right) : false }
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, @operator, @right = left, operator, right
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, @child_condition = :or, condition
52
+ @child_relation = :or
53
+ @child_condition = condition
49
54
  end
50
55
 
51
56
  def and(condition)
52
- @child_relation, @child_condition = :and, condition
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, right = context[left], context[right]
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
 
@@ -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
- SQUARE_BRACKETED = /\A\[(.*)\]\z/m
20
-
21
- def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = {})
22
- @environments = [environments].flatten
23
- @scopes = [(outer_scope || {})]
24
- @registers = registers
25
- @errors = []
26
- @resource_limits = (resource_limits || {}).merge!({ :render_score_current => 0, :assign_score_current => 0 })
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
- case e
100
- when SyntaxError
101
- "Liquid syntax error: #{e.message}"
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
- push(new_scope)
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
- def [](key)
153
- resolve(key)
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
- resolve(key) != nil
177
+ self[key] != nil
158
178
  end
159
179
 
160
- private
161
- LITERALS = {
162
- nil => nil, 'nil'.freeze => nil, 'null'.freeze => nil, ''.freeze => nil,
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
- # Fetches an object starting at the local scope and then moving up the hierachy
199
- def find_variable(key)
184
+ # Fetches an object starting at the local scope and then moving up the hierachy
185
+ def find_variable(key)
200
186
 
201
- # This was changed from find() to find_index() because this is a very hot
202
- # path and find_index() is optimized in MRI to reduce object allocation
203
- index = @scopes.find_index { |s| s.has_key?(key) }
204
- scope = @scopes[index] if index
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
- variable = nil
192
+ variable = nil
207
193
 
208
- if scope.nil?
209
- @environments.each do |e|
210
- variable = lookup_and_evaluate(e, key)
211
- unless variable.nil?
212
- scope = e
213
- break
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
- # Resolves namespaced queries gracefully.
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
- object = object.send(part.intern).to_liquid
207
+ variable = variable.to_liquid
208
+ variable.context = self if variable.respond_to?(:context=)
272
209
 
273
- # No key was present with the desired value and it wasn't one of the directly supported
274
- # keywords either. The only thing we got left is to return nil
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
- def lookup_and_evaluate(obj, key)
288
- if (value = obj[key]).is_a?(Proc) && obj.respond_to?(:[]=)
289
- obj[key] = (value.arity == 0) ? value.call : value.call(self)
290
- else
291
- value
292
- end
293
- end # lookup_and_evaluate
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|