module Liquid
class ContextError < StandardError
end
# Context keeps the variable stack and resolves variables, as well as keywords
#
# context['variable'] = 'testing'
# context['variable'] #=> 'testing'
# context['true'] #=> true
# context['10.2232'] #=> 10.2232
#
# context.stack do
# context['bob'] = 'bobsen'
# end
#
# context['bob'] #=> nil class Context
class Context
attr_reader :assigns
attr_accessor :registers
def initialize(assigns = {}, registers = nil)
@assigns = [assigns]
@registers = registers || {}
end
def strainer
@strainer ||= Strainer.create(self)
end
# adds filters to this context.
# this does not register the filters with the main Template object. see Template.register_filter
# for that
def add_filters(filter)
return unless filter.is_a?(Module)
strainer.extend(filter)
end
def invoke(method, *args)
if strainer.respond_to?(method)
strainer.__send__(method, *args)
else
return args[0]
end
end
# push new local scope on the stack. use Context#stack instead
def push
@assigns.unshift({})
end
# merge a hash of variables in the current local scope
def merge(new_assigns)
@assigns[0].merge!(new_assigns)
end
# pop from the stack. use Context#stack instead
def pop
raise ContextError if @assigns.size == 1
@assigns.shift
end
# pushes a new local scope on the stack, pops it at the end of the block
#
# Example:
#
# context.stack do
# context['var'] = 'hi'
# end
# context['var] #=> nil
#
def stack(&block)
push
begin
result = yield
ensure
pop
end
result
end
# Only allow String, Numeric, Hash, Array or Liquid::Drop
def []=(key, value)
@assigns[0][key] = value
end
def [](key)
resolve(key)
end
def has_key?(key)
resolve(key) != nil
end
private
# 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
nil
when 'true'
true
when 'false'
false
when 'empty'
:empty?
when 'nil', 'null'
nil
# Single quoted strings
when /^'(.*)'$/
$1.to_s
# Double quoted strings
when /^"(.*)"$/
$1.to_s
# Integer and floats
when /^(\d+)$/
$1.to_i
when /^(\d[\d\.]+)$/
$1.to_f
else
variable(key)
end
end
# fetches an object starting at the local scope and then moving up
# the hierachy
def fetch(key)
begin
for scope in @assigns
if scope.has_key?(key)
obj = scope[key]
if obj.is_a?(Liquid::Drop)
obj.context = self
end
return obj
end
end
rescue => e
raise ContextError, "Could not fetch key #{key} from context: " + e.message
end
nil
end
# resolves namespaced queries gracefully.
#
# Example
#
# @context['hash'] = {"name" => 'tobi'}
# assert_equal 'tobi', @context['hash.name']
def variable(key)
parts = key.split(VariableAttributeSeparator)
if object = fetch(parts.shift).to_liquid
object.context = self if object.is_a?(Liquid::Drop)
while not parts.size.zero?
next_part_name = parts.shift
# If the last part of the context variable is .size we just
# return the count of the objects in this object
if next_part_name == 'size' and parts.empty?
return object.size if object.is_a?(Array)
return object.size if object.is_a?(Hash) && !object.has_key?(next_part_name)
end
return nil if not object.respond_to?(:has_key?)
return nil if not object.has_key?(next_part_name)
object = object[next_part_name].to_liquid
object.context = self if object.is_a?(Liquid::Drop)
end
object
else
nil
end
end
end
end