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