From 9c183bea835ebe4d77ad2c47aa257a3cbd6ab1ff Mon Sep 17 00:00:00 2001 From: Jon Daniel Date: Tue, 21 Aug 2012 13:14:27 -0400 Subject: [PATCH] added interrupt class for continue/break statements When a continue or break statement is executed it pushes an interrupt to a stack in context. If any non-handled interrupts are present blocks will cease to execute. The for loop can handle the most recent interrupt in the stack. --- lib/liquid.rb | 1 + lib/liquid/block.rb | 22 ++++++-- lib/liquid/context.rb | 17 ++++++ lib/liquid/errors.rb | 5 -- lib/liquid/interrupts.rb | 17 ++++++ lib/liquid/tags/break.rb | 11 ++-- lib/liquid/tags/continue.rb | 9 ++-- lib/liquid/tags/for.rb | 17 +++--- test/liquid/tags/break_tag_test.rb | 16 ++++++ test/liquid/tags/continue_tag_test.rb | 16 ++++++ test/liquid/tags/for_tag_test.rb | 77 +++++++++++++++++++++++++-- 11 files changed, 171 insertions(+), 37 deletions(-) create mode 100644 lib/liquid/interrupts.rb create mode 100644 test/liquid/tags/break_tag_test.rb create mode 100644 test/liquid/tags/continue_tag_test.rb 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 3384249..b0add6f 100644 --- a/lib/liquid/errors.rb +++ b/lib/liquid/errors.rb @@ -8,9 +8,4 @@ module Liquid class StandardError < Error; end class SyntaxError < Error; end class StackLevelError < Error; end - - - class Interrupt < Error; end - class BreakInterrupt < Interrupt; end - class ContinueInterrupt < Interrupt; end 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 index 64758dd..d297612 100644 --- a/lib/liquid/tags/break.rb +++ b/lib/liquid/tags/break.rb @@ -9,15 +9,12 @@ module Liquid # {% endif %} # {% endfor %} # - class Break < Tag + class Break < Tag - ## - # Add an interrupt to context errors so a for loop can check - # for interrupts. - def render(context) - context.handle_error(BreakInterrupt.new) + def interrupt + BreakInterrupt.new end - + end Template.register_tag('break', Break) diff --git a/lib/liquid/tags/continue.rb b/lib/liquid/tags/continue.rb index e3cfe11..267ed9f 100644 --- a/lib/liquid/tags/continue.rb +++ b/lib/liquid/tags/continue.rb @@ -11,13 +11,10 @@ module Liquid # class Continue < Tag - ## - # Add an interrupt to context errors so a for loop can check - # for interrupts. - def render(context) - context.handle_error(ContinueInterrupt.new) + def interrupt + ContinueInterrupt.new end - + end Template.register_tag('continue', Continue) diff --git a/lib/liquid/tags/for.rb b/lib/liquid/tags/for.rb index 53888c2..8d2b27b 100644 --- a/lib/liquid/tags/for.rb +++ b/lib/liquid/tags/for.rb @@ -114,19 +114,14 @@ module Liquid 'first' => (index == 0), 'last' => (index == length - 1) } - rendered = render_all(@for_block, context) + result << render_all(@for_block, context) - if context.errors.last.is_a? BreakInterrupt - context.errors.pop - break + # 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 - - if context.errors.last.is_a? ContinueInterrupt - context.errors.pop - next - end - - result << rendered 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 a60c25f..edfdc88 100644 --- a/test/liquid/tags/for_tag_test.rb +++ b/test/liquid/tags/for_tag_test.rb @@ -168,18 +168,87 @@ HERE assert_template_result(expected,markup,assigns) end - def test_break + def test_for_with_break assigns = {'array' => {'items' => [1,2,3,4,5,6,7,8,9,10]}} - markup = '{% for i in array.items %}{{ i }}{% if i > 3 %}{% break %}{% endif %}{% endfor %}' - expected = "123" + + 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_continue + 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