From 4c0cfae0b725cd6945fcdd495b0c877b08b8a90b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20L=C3=BCtke?= Date: Thu, 8 May 2008 12:30:48 -0400 Subject: [PATCH] Changed implementation of For in such a way that it only depends on the existence of a each method. This allows drops to simply implement each for enumeration --- lib/liquid/block.rb | 3 +- lib/liquid/context.rb | 1 + lib/liquid/errors.rb | 1 + lib/liquid/tags/for.rb | 79 +++++++++++++++++++++---------------- lib/liquid/template.rb | 12 +++--- test/drop_test.rb | 14 +++++++ test/error_handling_test.rb | 15 +++++++ test/include_tag_test.rb | 7 ++-- test/standard_tag_test.rb | 9 +++-- 9 files changed, 94 insertions(+), 47 deletions(-) diff --git a/lib/liquid/block.rb b/lib/liquid/block.rb index e0a3421..e28cb4e 100644 --- a/lib/liquid/block.rb +++ b/lib/liquid/block.rb @@ -90,8 +90,7 @@ module Liquid token.respond_to?(:render) ? token.render(context) : token rescue Exception => e context.handle_error(e) - end - + end end end end diff --git a/lib/liquid/context.rb b/lib/liquid/context.rb index 4cfbb24..e420bf1 100644 --- a/lib/liquid/context.rb +++ b/lib/liquid/context.rb @@ -60,6 +60,7 @@ module Liquid # push new local scope on the stack. use Context#stack instead def push + raise StackLevelError, "Nesting too deep" if @scopes.length > 100 @scopes.unshift({}) end diff --git a/lib/liquid/errors.rb b/lib/liquid/errors.rb index 0d0343b..ce4ca7e 100644 --- a/lib/liquid/errors.rb +++ b/lib/liquid/errors.rb @@ -7,4 +7,5 @@ module Liquid class FileSystemError < Error; end class StandardError < Error; end class SyntaxError < Error; end + class StackLevelError < Error; end end \ No newline at end of file diff --git a/lib/liquid/tags/for.rb b/lib/liquid/tags/for.rb index 80c135f..80e2641 100644 --- a/lib/liquid/tags/for.rb +++ b/lib/liquid/tags/for.rb @@ -64,35 +64,31 @@ module Liquid collection = context[@collection_name] collection = collection.to_a if collection.is_a?(Range) - return '' if collection.nil? or collection.empty? - - range = (0..collection.length) - - if @attributes['limit'] or @attributes['offset'] - offset = 0 - if @attributes['offset'] == 'continue' - offset = context.registers[:for][@name] - else - offset = context[@attributes['offset']] || 0 - end - limit = context[@attributes['limit']] - - range_end = limit ? offset + limit : collection.length - range = (offset..range_end-1) - - # Save the range end in the registers so that future calls to - # offset:continue have something to pick up - context.registers[:for][@name] = range_end + return '' unless collection.respond_to?(:each) + + from = if @attributes['offset'] == 'continue' + context.registers[:for][@name].to_i + else + context[@attributes['offset']].to_i end - - result = [] - segment = collection[range] - return '' if segment.nil? - - context.stack do - length = segment.length + + limit = context[@attributes['limit']] + to = limit ? limit.to_i + from : nil + + + segment = slice_collection_using_each(collection, from, to) - segment.each_with_index do |item, index| + return '' if segment.empty? + + result = [] + + length = segment.length + + # Store our progress through the collection for the continue flag + context.registers[:for][@name] = from + segment.length + + context.stack do + segment.each_with_index do |item, index| context[@variable_name] = item context['forloop'] = { 'name' => @name, @@ -103,15 +99,32 @@ module Liquid 'rindex0' => length - index -1, 'first' => (index == 0), 'last' => (index == length - 1) } - + result << render_all(@nodelist, context) end end - - # Store position of last element we rendered. This allows us to do - - result - end + result + end + + def slice_collection_using_each(collection, from, to) + segments = [] + index = 0 + yielded = 0 + collection.each do |item| + + if to && to <= index + break + end + + if from <= index + segments << item + end + + index += 1 + end + + segments + end end Template.register_tag('for', For) diff --git a/lib/liquid/template.rb b/lib/liquid/template.rb index c2d9fa4..d07a6bb 100644 --- a/lib/liquid/template.rb +++ b/lib/liquid/template.rb @@ -83,7 +83,7 @@ module Liquid # filters and tags and might be useful to integrate liquid more with its host application # def render(*args) - return '' if @root.nil? + return '' if @root.nil? context = case args.first when Liquid::Context @@ -107,17 +107,17 @@ module Liquid if options[:filters] context.add_filters(options[:filters]) - end + end + when Module context.add_filters(args.pop) when Array context.add_filters(args.pop) end - - - # render the nodelist. - # for performance reasons we get a array back here. to_s will make a string out of it + begin + # render the nodelist. + # for performance reasons we get a array back here. join will make a string out of it @root.render(context).join ensure @errors = context.errors diff --git a/test/drop_test.rb b/test/drop_test.rb index 8a6921e..0b4b1e2 100644 --- a/test/drop_test.rb +++ b/test/drop_test.rb @@ -59,6 +59,16 @@ class ProductDrop < Liquid::Drop def callmenot "protected" end +end + +class EnumerableDrop < Liquid::Drop + include Enumerable + + def each + yield 1 + yield 2 + yield 3 + end end @@ -132,6 +142,10 @@ class DropsTest < Test::Unit::TestCase def test_access_context_from_drop assert_equal '123', Liquid::Template.parse( '{%for a in dummy%}{{ context.loop_pos }}{% endfor %}' ).render('context' => ContextDrop.new, 'dummy' => [1,2,3]) + end + + def test_enumerable_drop + assert_equal '123', Liquid::Template.parse( '{% for c in collection %}{{c}}{% endfor %}').render('collection' => EnumerableDrop.new) end diff --git a/test/error_handling_test.rb b/test/error_handling_test.rb index 515f642..5fee03d 100644 --- a/test/error_handling_test.rb +++ b/test/error_handling_test.rb @@ -57,8 +57,23 @@ class ErrorHandlingTest < Test::Unit::TestCase end + end + + def test_missing_endtag + + assert_nothing_raised do + + template = Liquid::Template.parse(' {% for a in b %} ... ') + assert_equal ' Liquid error: Unknown operator =! ', template.render + + assert_equal 1, template.errors.size + assert_equal Liquid::SyntaxError, template.errors.first.class + + end + end + def test_unrecognized_operator assert_nothing_raised do diff --git a/test/include_tag_test.rb b/test/include_tag_test.rb index cbeb8e1..06f3b30 100644 --- a/test/include_tag_test.rb +++ b/test/include_tag_test.rb @@ -96,9 +96,10 @@ class IncludeTagTest < Test::Unit::TestCase end Liquid::Template.file_system = infinite_file_system.new - - assert_match /-{552}Liquid error: stack level too deep$/, - Template.parse("{% include 'loop' %}").render + + assert_raise(Liquid::StackLevelError) do + Template.parse("{% include 'loop' %}").render! + end end diff --git a/test/standard_tag_test.rb b/test/standard_tag_test.rb index a927ec9..f0bc70b 100644 --- a/test/standard_tag_test.rb +++ b/test/standard_tag_test.rb @@ -104,11 +104,14 @@ HERE assert_template_result('12','{%for i in array limit:2 %}{{ i }}{%endfor%}',assigns) assert_template_result('1234','{%for i in array limit:4 %}{{ i }}{%endfor%}',assigns) assert_template_result('3456','{%for i in array limit:4 offset:2 %}{{ i }}{%endfor%}',assigns) - assert_template_result('3456','{%for i in array limit: 4 offset: 2 %}{{ i }}{%endfor%}',assigns) - + assert_template_result('3456','{%for i in array limit: 4 offset: 2 %}{{ i }}{%endfor%}',assigns) + end + + def test_dynamic_variable_limiting + assigns = {'array' => [1,2,3,4,5,6,7,8,9,0]} assigns['limit'] = 2 assigns['offset'] = 2 - assert_template_result('34','{%for i in array limit: limit offset: offset %}{{ i }}{%endfor%}',assigns) + assert_template_result('34','{%for i in array limit: limit offset: offset %}{{ i }}{%endfor%}',assigns) end def test_nested_for