diff --git a/lib/liquid/context.rb b/lib/liquid/context.rb
index 129b71a..809a817 100644
--- a/lib/liquid/context.rb
+++ b/lib/liquid/context.rb
@@ -1,5 +1,6 @@
module Liquid
+
# Context keeps the variable stack and resolves variables, as well as keywords
#
# context['variable'] = 'testing'
@@ -24,6 +25,7 @@ module Liquid
squash_instance_assigns_with_environments
@interrupts = []
+ @variable_cache = {}
end
def strainer
@@ -77,17 +79,20 @@ module Liquid
# Push new local scope on the stack. use Context#stack instead
def push(new_scope={})
+ @variable_cache = {}
@scopes.unshift(new_scope)
raise StackLevelError, "Nesting too deep" if @scopes.length > 100
end
# Merge a hash of variables in the current local scope
def merge(new_scopes)
+ @variable_cache = {}
@scopes[0].merge!(new_scopes)
end
# Pop from the stack. use Context#stack instead
def pop
+ @variable_cache = {}
raise ContextError if @scopes.size == 1
@scopes.shift
end
@@ -101,69 +106,112 @@ module Liquid
#
# context['var] #=> nil
def stack(new_scope={})
+ @variable_cache = {}
push(new_scope)
yield
ensure
pop
+ @variable_cache = {}
end
def clear_instance_assigns
+ @variable_cache = {}
@scopes[0] = {}
end
- # Only allow String, Numeric, Hash, Array, Proc, Boolean or Liquid::Drop
- def []=(key, value)
- @scopes[0][key] = value
+
+ # Look up variable, either resolve directly after considering the name. We can directly handle
+ # Strings, digits, floats and booleans (true,false).
+ # If no match is made we lookup the variable in the current scope and
+ # later move up to the parent blocks to see if we can resolve the variable somewhere up the tree.
+ # Some special keywords return symbols. Those symbols are to be called on the rhs object in expressions
+ #
+ # Example:
+ # products == empty #=> products.empty?
+ def resolve(key)
+ case key
+ when nil, ""
+ return nil
+ when "blank"
+ return :blank?
+ when "empty"
+ return :empty?
+ end
+
+ result = Parser.parse(key)
+ stack = []
+
+ result.each do |(sym, value)|
+
+ case sym
+ when :id
+ stack.push value
+ when :lookup
+ left = stack.pop
+ value = find_variable(left)
+
+ stack.push(harden(value))
+ when :range
+ right = stack.pop.to_i
+ left = stack.pop.to_i
+
+ stack.push (left..right)
+ when :buildin
+ left = stack.pop
+ value = invoke_buildin(left, value)
+
+ stack.push(harden(value))
+ when :call
+ left = stack.pop
+ right = stack.pop
+ value = lookup_and_evaluate(right, left)
+
+ stack.push(harden(value))
+ else
+ raise "unknown #{sym}"
+ end
+ end
+
+ return stack.first
end
- def [](key)
- resolve(key)
+
+ # Only allow String, Numeric, Hash, Array, Proc, Boolean or Liquid::Drop
+ def []=(key, value)
+ @variable_cache[key] = value
+ @scopes[0][key] = value
end
def has_key?(key)
resolve(key) != nil
end
- private
- LITERALS = {
- nil => nil, 'nil' => nil, 'null' => nil, '' => nil,
- 'true' => true,
- 'false' => false,
- 'blank' => :blank?,
- 'empty' => :empty?
- }
+ alias_method :[], :resolve
- # Look up variable, either resolve directly after considering the name. We can directly handle
- # Strings, digits, floats and booleans (true,false).
- # If no match is made we lookup the variable in the current scope and
- # later move up to the parent blocks to see if we can resolve the variable somewhere up the tree.
- # Some special keywords return symbols. Those symbols are to be called on the rhs object in expressions
- #
- # Example:
- # products == empty #=> products.empty?
- def resolve(key)
- if LITERALS.key?(key)
- LITERALS[key]
+ private
+
+ def invoke_buildin(obj, key)
+ # as weird as this is, liquid unit tests demand that we prioritize hash lookups
+ # to buildins. So if we got a hash and it has a :first element we need to call that
+ # instead of sending the first message...
+
+ if obj.respond_to?(:has_key?) && obj.has_key?(key)
+ return lookup_and_evaluate(obj, key)
+ end
+
+ if obj.respond_to?(key)
+ return obj.send(key)
else
- case key
- when /^'(.*)'$/ # Single quoted strings
- $1
- when /^"(.*)"$/ # Double quoted strings
- $1
- when /^(-?\d+)$/ # Integer and floats
- $1.to_i
- when /^\((\S+)\.\.(\S+)\)$/ # Ranges
- (resolve($1).to_i..resolve($2).to_i)
- when /^(-?\d[\d\.]+)$/ # Floats
- $1.to_f
- else
- variable(key)
- end
+ return nil
end
end
# Fetches an object starting at the local scope and then moving up the hierachy
def find_variable(key)
+ if val = @variable_cache[key]
+ return val
+ end
+
scope = @scopes.find { |s| s.has_key?(key) }
if scope.nil?
@@ -177,72 +225,36 @@ module Liquid
scope ||= @environments.last || @scopes.last
variable ||= lookup_and_evaluate(scope, key)
-
- variable = variable.to_liquid
- variable.context = self if variable.respond_to?(:context=)
+ @variable_cache[key] = variable
return variable
end
- # Resolves namespaced queries gracefully.
- #
- # Example
- # @context['hash'] = {"name" => 'tobi'}
- # assert_equal 'tobi', @context['hash.name']
- # assert_equal 'tobi', @context['hash["name"]']
- def variable(markup)
- parts = markup.scan(VariableParser)
- square_bracketed = /^\[(.*)\]$/
-
- first_part = parts.shift
-
- if first_part =~ square_bracketed
- first_part = resolve($1)
- end
-
- if object = find_variable(first_part)
-
- parts.each do |part|
- part = resolve($1) if part_resolved = (part =~ square_bracketed)
-
- # If object is a hash- or array-like object we look for the
- # presence of the key and if its available we return it
- if object.respond_to?(:[]) and
- ((object.respond_to?(:has_key?) and object.has_key?(part)) or
- (object.respond_to?(:fetch) and part.is_a?(Integer)))
-
- # if its a proc we will replace the entry with the proc
- res = lookup_and_evaluate(object, part)
- object = res.to_liquid
-
- # Some special cases. If the part wasn't in square brackets and
- # no key with the same name was found we interpret following calls
- # as commands and call them on the current object
- elsif !part_resolved and object.respond_to?(part) and ['size', 'first', 'last'].include?(part)
-
- object = object.send(part.intern).to_liquid
-
- # No key was present with the desired value and it wasn't one of the directly supported
- # keywords either. The only thing we got left is to return nil
- else
- return nil
- end
-
- # If we are dealing with a drop here we have to
- object.context = self if object.respond_to?(:context=)
- end
- end
-
- object
- end # variable
-
def lookup_and_evaluate(obj, key)
- if (value = obj[key]).is_a?(Proc) && obj.respond_to?(:[]=)
- obj[key] = (value.arity == 0) ? value.call : value.call(self)
- else
- value
+ return nil unless obj.respond_to?(:[])
+
+ if obj.is_a?(Array)
+ return nil unless key.is_a?(Integer)
end
- end # lookup_and_evaluate
+
+ value = obj[key]
+
+ if value.is_a?(Proc)
+ # call the proc
+ value = (value.arity == 0) ? value.call : value.call(self)
+
+ # memozie if possible
+ obj[key] = value if obj.respond_to?(:[]=)
+ end
+
+ value
+ end
+
+ def harden(value)
+ value = value.to_liquid
+ value.context = self if value.respond_to?(:context=)
+ return value
+ end
def squash_instance_assigns_with_environments
@scopes.last.each_key do |k|
@@ -254,6 +266,7 @@ module Liquid
end
end
end # squash_instance_assigns_with_environments
+
end # Context
end # Liquid