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.
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|