diff --git a/lib/liquid/context.rb b/lib/liquid/context.rb index 82eb843..d668122 100644 --- a/lib/liquid/context.rb +++ b/lib/liquid/context.rb @@ -1,13 +1,13 @@ module Liquid - + # 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.stack do # context['bob'] = 'bobsen' # end # @@ -15,49 +15,49 @@ module Liquid class Context attr_reader :scopes attr_reader :errors, :registers - + def initialize(assigns = {}, registers = {}, rethrow_errors = false) @scopes = [(assigns || {})] @registers = registers @errors = [] @rethrow_errors = rethrow_errors - end - + 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 + 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(filters) filters = [filters].flatten.compact - - filters.each do |f| + + filters.each do |f| raise ArgumentError, "Expected module but got: #{f.class}" unless f.is_a?(Module) strainer.extend(f) - end + end end - + def handle_error(e) errors.push(e) raise if @rethrow_errors - + case e - when SyntaxError - "Liquid syntax error: #{e.message}" - else + when SyntaxError + "Liquid syntax error: #{e.message}" + else "Liquid error: #{e.message}" end end - - + + def invoke(method, *args) if strainer.respond_to?(method) strainer.__send__(method, *args) else args.first - end + end end # push new local scope on the stack. use Context#stack instead @@ -65,23 +65,23 @@ module Liquid raise StackLevelError, "Nesting too deep" if @scopes.length > 100 @scopes.unshift({}) end - + # merge a hash of variables in the current local scope def merge(new_scopes) @scopes[0].merge!(new_scopes) end - + # pop from the stack. use Context#stack instead def pop - raise ContextError if @scopes.size == 1 + raise ContextError if @scopes.size == 1 @scopes.shift end - + # pushes a new local scope on the stack, pops it at the end of the block # # Example: # - # context.stack do + # context.stack do # context['var'] = 'hi' # end # context['var] #=> nil @@ -91,33 +91,33 @@ module Liquid push begin result = yield - ensure + ensure pop end - result + result end - + # Only allow String, Numeric, Hash, Array, Proc, Boolean or Liquid::Drop def []=(key, value) @scopes[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 + + # 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: + # Example: # # products == empty #=> products.empty? # @@ -128,9 +128,9 @@ module Liquid when 'true' true when 'false' - false + false when 'blank' - :blank? + :blank? when 'empty' :empty? # Single quoted strings @@ -138,27 +138,27 @@ module Liquid $1.to_s # Double quoted strings when /^"(.*)"$/ - $1.to_s + $1.to_s # Integer and floats - when /^(\d+)$/ + when /^(\d+)$/ $1.to_i # Ranges - when /^\((\S+)\.\.(\S+)\)$/ + when /^\((\S+)\.\.(\S+)\)$/ (resolve($1).to_i..resolve($2).to_i) # Floats - when /^(\d[\d\.]+)$/ + when /^(\d[\d\.]+)$/ $1.to_f else variable(key) - end + end end - - # fetches an object starting at the local scope and then moving up - # the hierachy + + # fetches an object starting at the local scope and then moving up + # the hierachy def find_variable(key) - @scopes.each do |scope| + @scopes.each do |scope| if scope.has_key?(key) - variable = scope[key] + variable = scope[key] variable = scope[key] = variable.call(self) if variable.is_a?(Proc) variable = variable.to_liquid variable.context = self if variable.respond_to?(:context=) @@ -169,9 +169,9 @@ module Liquid end # resolves namespaced queries gracefully. - # + # # Example - # + # # @context['hash'] = {"name" => 'tobi'} # assert_equal 'tobi', @context['hash.name'] # assert_equal 'tobi', @context['hash[name]'] @@ -179,65 +179,65 @@ module Liquid 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| - # If object is a hash we look for the presence of the key and if its available + if object = find_variable(first_part) + + parts.each do |part| + + # If object is a hash we look for the presence of the key and if its available # we return it - + if part =~ square_bracketed part = resolve($1) - + object[pos] = object[part].call(self) if object[part].is_a?(Proc) and object.respond_to?(:[]=) object = object[part].to_liquid - + else # Hash if object.respond_to?(:has_key?) and object.has_key?(part) - + # if its a proc we will replace the entry in the hash table with the proc res = object[part] res = object[part] = res.call(self) if res.is_a?(Proc) and object.respond_to?(:[]=) object = res.to_liquid # Array - elsif object.respond_to?(:fetch) and part =~ /^\d+$/ + elsif object.respond_to?(:fetch) and part =~ /^\d+$/ pos = part.to_i object[pos] = object[pos].call(self) if object[pos].is_a?(Proc) and object.respond_to?(:[]=) object = object[pos].to_liquid - + # Some special cases. If no key with the same name was found we interpret following calls # as commands and call them on the current object elsif 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 end - - # If we are dealing with a drop here we have to + + # If we are dealing with a drop here we have to object.context = self if object.respond_to?(:context=) end end - + object - end - + end + private - + def execute_proc(proc) proc.call(self) end diff --git a/test/context_test.rb b/test/context_test.rb index 1f63ad3..6fe27bb 100644 --- a/test/context_test.rb +++ b/test/context_test.rb @@ -9,7 +9,7 @@ class CentsDrop < Liquid::Drop def amount HundredCentes.new end - + def non_zero? true end @@ -52,41 +52,41 @@ class ContextTest < Test::Unit::TestCase def test_variables @context['string'] = 'string' assert_equal 'string', @context['string'] - + @context['num'] = 5 assert_equal 5, @context['num'] - + @context['time'] = Time.parse('2006-06-06 12:00:00') assert_equal Time.parse('2006-06-06 12:00:00'), @context['time'] - + @context['date'] = Date.today assert_equal Date.today, @context['date'] - + now = DateTime.now @context['datetime'] = now - assert_equal now, @context['datetime'] - + assert_equal now, @context['datetime'] + @context['bool'] = true - assert_equal true, @context['bool'] - + assert_equal true, @context['bool'] + @context['bool'] = false assert_equal false, @context['bool'] - + @context['nil'] = nil assert_equal nil, @context['nil'] - assert_equal nil, @context['nil'] + assert_equal nil, @context['nil'] end - + def test_variables_not_existing assert_equal nil, @context['does_not_exist'] end - + def test_scoping assert_nothing_raised do @context.push @context.pop end - + assert_raise(Liquid::ContextError) do @context.pop end @@ -97,71 +97,71 @@ class ContextTest < Test::Unit::TestCase @context.pop end end - + def test_length_query - + @context['numbers'] = [1,2,3,4] - + assert_equal 4, @context['numbers.size'] @context['numbers'] = {1 => 1,2 => 2,3 => 3,4 => 4} assert_equal 4, @context['numbers.size'] - + @context['numbers'] = {1 => 1,2 => 2,3 => 3,4 => 4, 'size' => 1000} assert_equal 1000, @context['numbers.size'] - - end - + + end + def test_hyphenated_variable @context['oh-my'] = 'godz' assert_equal 'godz', @context['oh-my'] - + end - + def test_add_filter - - filter = Module.new do + + filter = Module.new do def hi(output) output + ' hi!' end end - + context = Context.new(@template) context.add_filters(filter) assert_equal 'hi? hi!', context.invoke(:hi, 'hi?') - + context = Context.new(@template) assert_equal 'hi?', context.invoke(:hi, 'hi?') context.add_filters(filter) assert_equal 'hi? hi!', context.invoke(:hi, 'hi?') - + end - + def test_override_global_filter - global = Module.new do + global = Module.new do def notice(output) "Global #{output}" end end - - local = Module.new do + + local = Module.new do def notice(output) "Local #{output}" end end - - Template.register_filter(global) + + Template.register_filter(global) assert_equal 'Global test', Template.parse("{{'test' | notice }}").render assert_equal 'Local test', Template.parse("{{'test' | notice }}").render({}, :filters => [local]) end - + def test_only_intended_filters_make_it_there - filter = Module.new do + filter = Module.new do def hi(output) output + ' hi!' end @@ -172,28 +172,28 @@ class ContextTest < Test::Unit::TestCase context.add_filters(filter) assert_equal (methods + ['hi']).sort, context.strainer.methods.sort end - + def test_add_item_in_outer_scope @context['test'] = 'test' @context.push assert_equal 'test', @context['test'] - @context.pop - assert_equal 'test', @context['test'] + @context.pop + assert_equal 'test', @context['test'] end def test_add_item_in_inner_scope @context.push @context['test'] = 'test' assert_equal 'test', @context['test'] - @context.pop - assert_equal nil, @context['test'] + @context.pop + assert_equal nil, @context['test'] end - + def test_hierachical_data @context['hash'] = {"name" => 'tobi'} assert_equal 'tobi', @context['hash.name'] end - + def test_keywords assert_equal true, @context['true'] assert_equal false, @context['false'] @@ -203,20 +203,20 @@ class ContextTest < Test::Unit::TestCase assert_equal 100, @context['100'] assert_equal 100.00, @context['100.00'] end - + def test_strings assert_equal "hello!", @context['"hello!"'] assert_equal "hello!", @context["'hello!'"] - end - + end + def test_merge @context.merge({ "test" => "test" }) assert_equal 'test', @context['test'] @context.merge({ "test" => "newvalue", "foo" => "bar" }) assert_equal 'newvalue', @context['test'] - assert_equal 'bar', @context['foo'] + assert_equal 'bar', @context['foo'] end - + def test_array_notation @context['test'] = [1,2,3,4,5] @@ -224,49 +224,49 @@ class ContextTest < Test::Unit::TestCase assert_equal 2, @context['test[1]'] assert_equal 3, @context['test[2]'] assert_equal 4, @context['test[3]'] - assert_equal 5, @context['test[4]'] + assert_equal 5, @context['test[4]'] end - + def test_recoursive_array_notation @context['test'] = {'test' => [1,2,3,4,5]} assert_equal 1, @context['test.test[0]'] - + @context['test'] = [{'test' => 'worked'}] - assert_equal 'worked', @context['test[0].test'] + assert_equal 'worked', @context['test[0].test'] end - + def test_hash_to_array_transition @context['colors'] = { 'Blue' => ['003366','336699', '6699CC', '99CCFF'], 'Green' => ['003300','336633', '669966', '99CC99'], 'Yellow' => ['CC9900','FFCC00', 'FFFF99', 'FFFFCC'], 'Red' => ['660000','993333', 'CC6666', 'FF9999'] - } + } assert_equal '003366', @context['colors.Blue[0]'] assert_equal 'FF9999', @context['colors.Red[3]'] end - + def test_try_first @context['test'] = [1,2,3,4,5] assert_equal 1, @context['test.first'] assert_equal 5, @context['test.last'] - + @context['test'] = {'test' => [1,2,3,4,5]} - + assert_equal 1, @context['test.test.first'] - assert_equal 5, @context['test.test.last'] - + assert_equal 5, @context['test.test.last'] + @context['test'] = [1] assert_equal 1, @context['test.first'] - assert_equal 1, @context['test.last'] + assert_equal 1, @context['test.last'] end def test_access_hashes_with_hash_notation - + @context['products'] = {'count' => 5, 'tags' => ['deepsnow', 'freestyle'] } @context['product'] = {'variants' => [ {'title' => 'draft151cm'}, {'title' => 'element151cm'} ]} @@ -279,17 +279,17 @@ class ContextTest < Test::Unit::TestCase assert_equal 'draft151cm', @context['product["variants"][0]["title"]'] assert_equal 'element151cm', @context['product["variants"].last["title"]'] end - + def test_access_variable_with_hash_notation @context['foo'] = 'baz' @context['bar'] = 'foo' - + assert_equal 'baz', @context['["foo"]'] assert_equal 'baz', @context['[bar]'] end - + def test_access_hashes_with_hash_access_variables - + @context['var'] = 'tags' @context['nested'] = {'var' => 'tags'} @context['products'] = {'count' => 5, 'tags' => ['deepsnow', 'freestyle'] } @@ -297,7 +297,7 @@ class ContextTest < Test::Unit::TestCase assert_equal 'deepsnow', @context['products[var].first'] assert_equal 'freestyle', @context['products[nested.var].last'] end - + def test_first_can_appear_in_middle_of_callchain @@ -307,9 +307,9 @@ class ContextTest < Test::Unit::TestCase assert_equal 'element151cm', @context['product.variants[1].title'] assert_equal 'draft151cm', @context['product.variants.first.title'] assert_equal 'element151cm', @context['product.variants.last.title'] - + end - + def test_cents @context.merge( "cents" => HundredCentes.new ) assert_equal 100, @context['cents'] @@ -317,37 +317,37 @@ class ContextTest < Test::Unit::TestCase def test_nested_cents @context.merge( "cents" => { 'amount' => HundredCentes.new} ) - assert_equal 100, @context['cents.amount'] + assert_equal 100, @context['cents.amount'] @context.merge( "cents" => { 'cents' => { 'amount' => HundredCentes.new} } ) - assert_equal 100, @context['cents.cents.amount'] + assert_equal 100, @context['cents.cents.amount'] end - + def test_cents_through_drop @context.merge( "cents" => CentsDrop.new ) - assert_equal 100, @context['cents.amount'] + assert_equal 100, @context['cents.amount'] end - + def test_nested_cents_through_drop @context.merge( "vars" => {"cents" => CentsDrop.new} ) - assert_equal 100, @context['vars.cents.amount'] + assert_equal 100, @context['vars.cents.amount'] end - + def test_drop_methods_with_question_marks @context.merge( "cents" => CentsDrop.new ) - assert @context['cents.non_zero?'] + assert @context['cents.non_zero?'] end def test_context_from_within_drop @context.merge( "test" => '123', "vars" => ContextSensitiveDrop.new ) - assert_equal '123', @context['vars.test'] + assert_equal '123', @context['vars.test'] end def test_nested_context_from_within_drop @context.merge( "test" => '123', "vars" => {"local" => ContextSensitiveDrop.new } ) - assert_equal '123', @context['vars.local.test'] + assert_equal '123', @context['vars.local.test'] end - + def test_ranges @context.merge( "test" => '5' ) assert_equal (1..5), @context['(1..5)'] @@ -357,62 +357,62 @@ class ContextTest < Test::Unit::TestCase def test_cents_through_drop_nestedly @context.merge( "cents" => {"cents" => CentsDrop.new} ) - assert_equal 100, @context['cents.cents.amount'] + assert_equal 100, @context['cents.cents.amount'] @context.merge( "cents" => { "cents" => {"cents" => CentsDrop.new}} ) - assert_equal 100, @context['cents.cents.cents.amount'] + assert_equal 100, @context['cents.cents.cents.amount'] end - + def test_proc_as_variable @context['dynamic'] = Proc.new { 'Hello' } - + assert_equal 'Hello', @context['dynamic'] end def test_lambda_as_variable @context['dynamic'] = lambda { 'Hello' } - + assert_equal 'Hello', @context['dynamic'] end def test_nested_lambda_as_variable @context['dynamic'] = { "lambda" => lambda { 'Hello' } } - + assert_equal 'Hello', @context['dynamic.lambda'] end - + def test_lambda_is_called_once @context['callcount'] = lambda { @global ||= 0; @global += 1; @global.to_s } - + assert_equal '1', @context['callcount'] assert_equal '1', @context['callcount'] - assert_equal '1', @context['callcount'] - + assert_equal '1', @context['callcount'] + @global = nil end def test_nested_lambda_is_called_once @context['callcount'] = { "lambda" => lambda { @global ||= 0; @global += 1; @global.to_s } } - + + assert_equal '1', @context['callcount.lambda'] assert_equal '1', @context['callcount.lambda'] assert_equal '1', @context['callcount.lambda'] - assert_equal '1', @context['callcount.lambda'] @global = nil end - + def test_access_to_context_from_proc @context.registers[:magic] = 345392 - + @context['magic'] = lambda { @context.registers[:magic] } - - assert_equal 345392, @context['magic'] + + assert_equal 345392, @context['magic'] end - + def test_to_liquid_and_context_at_first_level @context['category'] = Category.new("foobar") assert_kind_of CategoryDrop, @context['category'] assert_equal @context, @context['category'].context end -end \ No newline at end of file +end