diff --git a/lib/liquid.rb b/lib/liquid.rb index 7c4b327..82676d0 100644 --- a/lib/liquid.rb +++ b/lib/liquid.rb @@ -48,6 +48,7 @@ end require 'liquid/drop' require 'liquid/extensions' require 'liquid/errors' +require 'liquid/interrupts' require 'liquid/strainer' require 'liquid/context' require 'liquid/tag' diff --git a/lib/liquid/block.rb b/lib/liquid/block.rb index a98874f..8ea87b0 100644 --- a/lib/liquid/block.rb +++ b/lib/liquid/block.rb @@ -89,13 +89,27 @@ module Liquid end def render_all(list, context) - list.collect do |token| + output = [] + list.each do |token| + # Break out if we have any unhanded interrupts. + break if context.has_interrupt? + begin - token.respond_to?(:render) ? token.render(context) : token + # If we get an Interrupt that means the block must stop processing. An + # Interrupt is any command that stops block execution such as {% break %} + # or {% continue %} + if token.is_a? Continue or token.is_a? Break + context.push_interrupt(token.interrupt) + break + end + + output << (token.respond_to?(:render) ? token.render(context) : token) rescue ::StandardError => e - context.handle_error(e) + output << (context.handle_error(e)) end - end.join + end + + output.join end end end diff --git a/lib/liquid/context.rb b/lib/liquid/context.rb index 1d5290a..4500aef 100644 --- a/lib/liquid/context.rb +++ b/lib/liquid/context.rb @@ -22,6 +22,8 @@ module Liquid @errors = [] @rethrow_errors = rethrow_errors squash_instance_assigns_with_environments + + @interrupts = [] end def strainer @@ -41,6 +43,21 @@ module Liquid end end + # are there any not handled interrupts? + def has_interrupt? + !@interrupts.empty? + end + + # push an interrupt to the stack. this interrupt is considered not handled. + def push_interrupt(e) + @interrupts.push(e) + end + + # pop an interrupt from the stack + def pop_interrupt + @interrupts.pop + end + def handle_error(e) errors.push(e) raise if @rethrow_errors diff --git a/lib/liquid/errors.rb b/lib/liquid/errors.rb index ce4ca7e..b0add6f 100644 --- a/lib/liquid/errors.rb +++ b/lib/liquid/errors.rb @@ -8,4 +8,4 @@ module Liquid class StandardError < Error; end class SyntaxError < Error; end class StackLevelError < Error; end -end \ No newline at end of file +end diff --git a/lib/liquid/interrupts.rb b/lib/liquid/interrupts.rb new file mode 100644 index 0000000..d6bf61e --- /dev/null +++ b/lib/liquid/interrupts.rb @@ -0,0 +1,17 @@ +module Liquid + + # An interrupt is any command that breaks processing of a block (ex: a for loop). + class Interrupt + attr_reader :message + + def initialize(message=nil) + @message = message || "interrupt" + end + end + + # Interrupt that is thrown whenever a {% break %} is called. + class BreakInterrupt < Interrupt; end + + # Interrupt that is thrown whenever a {% continue %} is called. + class ContinueInterrupt < Interrupt; end +end diff --git a/lib/liquid/tags/break.rb b/lib/liquid/tags/break.rb new file mode 100644 index 0000000..d297612 --- /dev/null +++ b/lib/liquid/tags/break.rb @@ -0,0 +1,21 @@ +module Liquid + + # Break tag to be used to break out of a for loop. + # + # == Basic Usage: + # {% for item in collection %} + # {% if item.condition %} + # {% break %} + # {% endif %} + # {% endfor %} + # + class Break < Tag + + def interrupt + BreakInterrupt.new + end + + end + + Template.register_tag('break', Break) +end diff --git a/lib/liquid/tags/continue.rb b/lib/liquid/tags/continue.rb new file mode 100644 index 0000000..267ed9f --- /dev/null +++ b/lib/liquid/tags/continue.rb @@ -0,0 +1,21 @@ +module Liquid + + # Continue tag to be used to break out of a for loop. + # + # == Basic Usage: + # {% for item in collection %} + # {% if item.condition %} + # {% continue %} + # {% endif %} + # {% endfor %} + # + class Continue < Tag + + def interrupt + ContinueInterrupt.new + end + + end + + Template.register_tag('continue', Continue) +end diff --git a/lib/liquid/tags/for.rb b/lib/liquid/tags/for.rb index a76b875..8d2b27b 100644 --- a/lib/liquid/tags/for.rb +++ b/lib/liquid/tags/for.rb @@ -69,7 +69,7 @@ module Liquid @nodelist = @else_block = [] end - def render(context) + def render(context) context.registers[:for] ||= Hash.new(0) collection = context[@collection_name] @@ -101,8 +101,8 @@ module Liquid # 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.stack do + segment.each_with_index do |item, index| context[@variable_name] = item context['forloop'] = { 'name' => @name, @@ -115,6 +115,13 @@ module Liquid 'last' => (index == length - 1) } result << render_all(@for_block, context) + + # Handle any interrupts if they exist. + if context.has_interrupt? + interrupt = context.pop_interrupt + break if interrupt.is_a? BreakInterrupt + next if interrupt.is_a? ContinueInterrupt + end end end result diff --git a/test/liquid/tags/break_tag_test.rb b/test/liquid/tags/break_tag_test.rb new file mode 100644 index 0000000..cbe3095 --- /dev/null +++ b/test/liquid/tags/break_tag_test.rb @@ -0,0 +1,16 @@ +require 'test_helper' + +class BreakTagTest < Test::Unit::TestCase + include Liquid + + # tests that no weird errors are raised if break is called outside of a + # block + def test_break_with_no_block + assigns = {'i' => 1} + markup = '{% break %}' + expected = '' + + assert_template_result(expected, markup, assigns) + end + +end diff --git a/test/liquid/tags/continue_tag_test.rb b/test/liquid/tags/continue_tag_test.rb new file mode 100644 index 0000000..5825a23 --- /dev/null +++ b/test/liquid/tags/continue_tag_test.rb @@ -0,0 +1,16 @@ +require 'test_helper' + +class ContinueTagTest < Test::Unit::TestCase + include Liquid + + # tests that no weird errors are raised if continue is called outside of a + # block + def test_continue_with_no_block + assigns = {} + markup = '{% continue %}' + expected = '' + + assert_template_result(expected, markup, assigns) + end + +end diff --git a/test/liquid/tags/for_tag_test.rb b/test/liquid/tags/for_tag_test.rb index 485701d..edfdc88 100644 --- a/test/liquid/tags/for_tag_test.rb +++ b/test/liquid/tags/for_tag_test.rb @@ -168,6 +168,88 @@ HERE assert_template_result(expected,markup,assigns) end + def test_for_with_break + assigns = {'array' => {'items' => [1,2,3,4,5,6,7,8,9,10]}} + + markup = '{% for i in array.items %}{% break %}{% endfor %}' + expected = "" + assert_template_result(expected,markup,assigns) + + markup = '{% for i in array.items %}{{ i }}{% break %}{% endfor %}' + expected = "1" + assert_template_result(expected,markup,assigns) + + markup = '{% for i in array.items %}{% break %}{{ i }}{% endfor %}' + expected = "" + assert_template_result(expected,markup,assigns) + + markup = '{% for i in array.items %}{{ i }}{% if i > 3 %}{% break %}{% endif %}{% endfor %}' + expected = "1234" + assert_template_result(expected,markup,assigns) + + # tests to ensure it only breaks out of the local for loop + # and not all of them. + assigns = {'array' => [[1,2],[3,4],[5,6]] } + markup = '{% for item in array %}' + + '{% for i in item %}' + + '{% if i == 1 %}' + + '{% break %}' + + '{% endif %}' + + '{{ i }}' + + '{% endfor %}' + + '{% endfor %}' + expected = '3456' + assert_template_result(expected, markup, assigns) + + # test break does nothing when unreached + assigns = {'array' => {'items' => [1,2,3,4,5]}} + markup = '{% for i in array.items %}{% if i == 9999 %}{% break %}{% endif %}{{ i }}{% endfor %}' + expected = '12345' + assert_template_result(expected, markup, assigns) + end + + def test_for_with_continue + assigns = {'array' => {'items' => [1,2,3,4,5]}} + + markup = '{% for i in array.items %}{% continue %}{% endfor %}' + expected = "" + assert_template_result(expected,markup,assigns) + + markup = '{% for i in array.items %}{{ i }}{% continue %}{% endfor %}' + expected = "12345" + assert_template_result(expected,markup,assigns) + + markup = '{% for i in array.items %}{% continue %}{{ i }}{% endfor %}' + expected = "" + assert_template_result(expected,markup,assigns) + + markup = '{% for i in array.items %}{% if i > 3 %}{% continue %}{% endif %}{{ i }}{% endfor %}' + expected = "123" + assert_template_result(expected,markup,assigns) + + markup = '{% for i in array.items %}{% if i == 3 %}{% continue %}{% else %}{{ i }}{% endif %}{% endfor %}' + expected = "1245" + assert_template_result(expected,markup,assigns) + + # tests to ensure it only continues the local for loop and not all of them. + assigns = {'array' => [[1,2],[3,4],[5,6]] } + markup = '{% for item in array %}' + + '{% for i in item %}' + + '{% if i == 1 %}' + + '{% continue %}' + + '{% endif %}' + + '{{ i }}' + + '{% endfor %}' + + '{% endfor %}' + expected = '23456' + assert_template_result(expected, markup, assigns) + + # test continue does nothing when unreached + assigns = {'array' => {'items' => [1,2,3,4,5]}} + markup = '{% for i in array.items %}{% if i == 9999 %}{% continue %}{% endif %}{{ i }}{% endfor %}' + expected = '12345' + assert_template_result(expected, markup, assigns) + end def test_for_tag_string # ruby 1.8.7 "String".each => Enumerator with single "String" element.