mirror of
https://github.com/Shopify/liquid.git
synced 2026-09-12 23:40:45 -07:00
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.
This commit is contained in:
@@ -48,6 +48,7 @@ end
|
|||||||
require 'liquid/drop'
|
require 'liquid/drop'
|
||||||
require 'liquid/extensions'
|
require 'liquid/extensions'
|
||||||
require 'liquid/errors'
|
require 'liquid/errors'
|
||||||
|
require 'liquid/interrupts'
|
||||||
require 'liquid/strainer'
|
require 'liquid/strainer'
|
||||||
require 'liquid/context'
|
require 'liquid/context'
|
||||||
require 'liquid/tag'
|
require 'liquid/tag'
|
||||||
|
|||||||
+18
-4
@@ -89,13 +89,27 @@ module Liquid
|
|||||||
end
|
end
|
||||||
|
|
||||||
def render_all(list, context)
|
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
|
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
|
rescue ::StandardError => e
|
||||||
context.handle_error(e)
|
output << (context.handle_error(e))
|
||||||
end
|
end
|
||||||
end.join
|
end
|
||||||
|
|
||||||
|
output.join
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ module Liquid
|
|||||||
@errors = []
|
@errors = []
|
||||||
@rethrow_errors = rethrow_errors
|
@rethrow_errors = rethrow_errors
|
||||||
squash_instance_assigns_with_environments
|
squash_instance_assigns_with_environments
|
||||||
|
|
||||||
|
@interrupts = []
|
||||||
end
|
end
|
||||||
|
|
||||||
def strainer
|
def strainer
|
||||||
@@ -41,6 +43,21 @@ module Liquid
|
|||||||
end
|
end
|
||||||
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)
|
def handle_error(e)
|
||||||
errors.push(e)
|
errors.push(e)
|
||||||
raise if @rethrow_errors
|
raise if @rethrow_errors
|
||||||
|
|||||||
@@ -8,9 +8,4 @@ module Liquid
|
|||||||
class StandardError < Error; end
|
class StandardError < Error; end
|
||||||
class SyntaxError < Error; end
|
class SyntaxError < Error; end
|
||||||
class StackLevelError < Error; end
|
class StackLevelError < Error; end
|
||||||
|
|
||||||
|
|
||||||
class Interrupt < Error; end
|
|
||||||
class BreakInterrupt < Interrupt; end
|
|
||||||
class ContinueInterrupt < Interrupt; end
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -9,15 +9,12 @@ module Liquid
|
|||||||
# {% endif %}
|
# {% endif %}
|
||||||
# {% endfor %}
|
# {% endfor %}
|
||||||
#
|
#
|
||||||
class Break < Tag
|
class Break < Tag
|
||||||
|
|
||||||
##
|
def interrupt
|
||||||
# Add an interrupt to context errors so a for loop can check
|
BreakInterrupt.new
|
||||||
# for interrupts.
|
|
||||||
def render(context)
|
|
||||||
context.handle_error(BreakInterrupt.new)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
Template.register_tag('break', Break)
|
Template.register_tag('break', Break)
|
||||||
|
|||||||
@@ -11,13 +11,10 @@ module Liquid
|
|||||||
#
|
#
|
||||||
class Continue < Tag
|
class Continue < Tag
|
||||||
|
|
||||||
##
|
def interrupt
|
||||||
# Add an interrupt to context errors so a for loop can check
|
ContinueInterrupt.new
|
||||||
# for interrupts.
|
|
||||||
def render(context)
|
|
||||||
context.handle_error(ContinueInterrupt.new)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
Template.register_tag('continue', Continue)
|
Template.register_tag('continue', Continue)
|
||||||
|
|||||||
+6
-11
@@ -114,19 +114,14 @@ module Liquid
|
|||||||
'first' => (index == 0),
|
'first' => (index == 0),
|
||||||
'last' => (index == length - 1) }
|
'last' => (index == length - 1) }
|
||||||
|
|
||||||
rendered = render_all(@for_block, context)
|
result << render_all(@for_block, context)
|
||||||
|
|
||||||
if context.errors.last.is_a? BreakInterrupt
|
# Handle any interrupts if they exist.
|
||||||
context.errors.pop
|
if context.has_interrupt?
|
||||||
break
|
interrupt = context.pop_interrupt
|
||||||
|
break if interrupt.is_a? BreakInterrupt
|
||||||
|
next if interrupt.is_a? ContinueInterrupt
|
||||||
end
|
end
|
||||||
|
|
||||||
if context.errors.last.is_a? ContinueInterrupt
|
|
||||||
context.errors.pop
|
|
||||||
next
|
|
||||||
end
|
|
||||||
|
|
||||||
result << rendered
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
result
|
result
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -168,18 +168,87 @@ HERE
|
|||||||
assert_template_result(expected,markup,assigns)
|
assert_template_result(expected,markup,assigns)
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_break
|
def test_for_with_break
|
||||||
assigns = {'array' => {'items' => [1,2,3,4,5,6,7,8,9,10]}}
|
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)
|
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
|
end
|
||||||
|
|
||||||
def test_continue
|
def test_for_with_continue
|
||||||
assigns = {'array' => {'items' => [1,2,3,4,5]}}
|
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 %}'
|
markup = '{% for i in array.items %}{% if i == 3 %}{% continue %}{% else %}{{ i }}{% endif %}{% endfor %}'
|
||||||
expected = "1245"
|
expected = "1245"
|
||||||
assert_template_result(expected,markup,assigns)
|
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
|
end
|
||||||
|
|
||||||
def test_for_tag_string
|
def test_for_tag_string
|
||||||
|
|||||||
Reference in New Issue
Block a user