liquid 2.4.1 → 2.5.0
Sign up to get free protection for your applications and to get access to all the features.
- data/History.md +8 -0
- data/lib/liquid/block.rb +18 -4
- data/lib/liquid/context.rb +19 -5
- data/lib/liquid/drop.rb +14 -2
- data/lib/liquid/errors.rb +1 -1
- data/lib/liquid/interrupts.rb +17 -0
- data/lib/liquid/strainer.rb +25 -26
- data/lib/liquid/tags/break.rb +21 -0
- data/lib/liquid/tags/continue.rb +21 -0
- data/lib/liquid/tags/for.rb +10 -3
- data/lib/liquid/variable.rb +13 -6
- data/lib/liquid.rb +1 -0
- data/test/liquid/block_test.rb +1 -1
- data/test/liquid/context_test.rb +3 -3
- data/test/liquid/drop_test.rb +7 -0
- data/test/liquid/filter_test.rb +13 -0
- data/test/liquid/security_test.rb +23 -0
- data/test/liquid/strainer_test.rb +38 -11
- data/test/liquid/tags/break_tag_test.rb +16 -0
- data/test/liquid/tags/continue_tag_test.rb +16 -0
- data/test/liquid/tags/for_tag_test.rb +82 -0
- data/test/liquid/variable_test.rb +24 -14
- metadata +13 -6
data/History.md
CHANGED
@@ -1,5 +1,13 @@
|
|
1
1
|
# Liquid Version History
|
2
2
|
|
3
|
+
## 2.5.0 / 2013-03-06
|
4
|
+
|
5
|
+
* Prevent Object methods from being called on drops
|
6
|
+
* Avoid symbol injection from liquid
|
7
|
+
* Added break and continue statements
|
8
|
+
* Fix filter parser for args without space separators
|
9
|
+
* Add support for filter keyword arguments
|
10
|
+
|
3
11
|
## 2.4.0 / 2012-08-03
|
4
12
|
|
5
13
|
* Performance improvements
|
data/lib/liquid/block.rb
CHANGED
@@ -89,13 +89,27 @@ module Liquid
|
|
89
89
|
end
|
90
90
|
|
91
91
|
def render_all(list, context)
|
92
|
-
|
92
|
+
output = []
|
93
|
+
list.each do |token|
|
94
|
+
# Break out if we have any unhanded interrupts.
|
95
|
+
break if context.has_interrupt?
|
96
|
+
|
93
97
|
begin
|
94
|
-
|
98
|
+
# If we get an Interrupt that means the block must stop processing. An
|
99
|
+
# Interrupt is any command that stops block execution such as {% break %}
|
100
|
+
# or {% continue %}
|
101
|
+
if token.is_a? Continue or token.is_a? Break
|
102
|
+
context.push_interrupt(token.interrupt)
|
103
|
+
break
|
104
|
+
end
|
105
|
+
|
106
|
+
output << (token.respond_to?(:render) ? token.render(context) : token)
|
95
107
|
rescue ::StandardError => e
|
96
|
-
context.handle_error(e)
|
108
|
+
output << (context.handle_error(e))
|
97
109
|
end
|
98
|
-
end
|
110
|
+
end
|
111
|
+
|
112
|
+
output.join
|
99
113
|
end
|
100
114
|
end
|
101
115
|
end
|
data/lib/liquid/context.rb
CHANGED
@@ -22,6 +22,8 @@ module Liquid
|
|
22
22
|
@errors = []
|
23
23
|
@rethrow_errors = rethrow_errors
|
24
24
|
squash_instance_assigns_with_environments
|
25
|
+
|
26
|
+
@interrupts = []
|
25
27
|
end
|
26
28
|
|
27
29
|
def strainer
|
@@ -37,10 +39,26 @@ module Liquid
|
|
37
39
|
|
38
40
|
filters.each do |f|
|
39
41
|
raise ArgumentError, "Expected module but got: #{f.class}" unless f.is_a?(Module)
|
42
|
+
Strainer.add_known_filter(f)
|
40
43
|
strainer.extend(f)
|
41
44
|
end
|
42
45
|
end
|
43
46
|
|
47
|
+
# are there any not handled interrupts?
|
48
|
+
def has_interrupt?
|
49
|
+
!@interrupts.empty?
|
50
|
+
end
|
51
|
+
|
52
|
+
# push an interrupt to the stack. this interrupt is considered not handled.
|
53
|
+
def push_interrupt(e)
|
54
|
+
@interrupts.push(e)
|
55
|
+
end
|
56
|
+
|
57
|
+
# pop an interrupt from the stack
|
58
|
+
def pop_interrupt
|
59
|
+
@interrupts.pop
|
60
|
+
end
|
61
|
+
|
44
62
|
def handle_error(e)
|
45
63
|
errors.push(e)
|
46
64
|
raise if @rethrow_errors
|
@@ -54,11 +72,7 @@ module Liquid
|
|
54
72
|
end
|
55
73
|
|
56
74
|
def invoke(method, *args)
|
57
|
-
|
58
|
-
strainer.__send__(method, *args)
|
59
|
-
else
|
60
|
-
args.first
|
61
|
-
end
|
75
|
+
strainer.invoke(method, *args)
|
62
76
|
end
|
63
77
|
|
64
78
|
# Push new local scope on the stack. use <tt>Context#stack</tt> instead
|
data/lib/liquid/drop.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
1
3
|
module Liquid
|
2
4
|
|
3
5
|
# A drop in liquid is a class which allows you to export DOM like things to liquid.
|
@@ -22,6 +24,8 @@ module Liquid
|
|
22
24
|
class Drop
|
23
25
|
attr_writer :context
|
24
26
|
|
27
|
+
EMPTY_STRING = ''.freeze
|
28
|
+
|
25
29
|
# Catch all for the method
|
26
30
|
def before_method(method)
|
27
31
|
nil
|
@@ -29,8 +33,8 @@ module Liquid
|
|
29
33
|
|
30
34
|
# called by liquid to invoke a drop
|
31
35
|
def invoke_drop(method_or_key)
|
32
|
-
if method_or_key && method_or_key !=
|
33
|
-
send(method_or_key
|
36
|
+
if method_or_key && method_or_key != EMPTY_STRING && self.class.invokable?(method_or_key)
|
37
|
+
send(method_or_key)
|
34
38
|
else
|
35
39
|
before_method(method_or_key)
|
36
40
|
end
|
@@ -45,5 +49,13 @@ module Liquid
|
|
45
49
|
end
|
46
50
|
|
47
51
|
alias :[] :invoke_drop
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
# Check for method existence without invoking respond_to?, which creates symbols
|
56
|
+
def self.invokable?(method_name)
|
57
|
+
@invokable_methods ||= Set.new((public_instance_methods - Liquid::Drop.public_instance_methods).map(&:to_s))
|
58
|
+
@invokable_methods.include?(method_name.to_s)
|
59
|
+
end
|
48
60
|
end
|
49
61
|
end
|
data/lib/liquid/errors.rb
CHANGED
@@ -0,0 +1,17 @@
|
|
1
|
+
module Liquid
|
2
|
+
|
3
|
+
# An interrupt is any command that breaks processing of a block (ex: a for loop).
|
4
|
+
class Interrupt
|
5
|
+
attr_reader :message
|
6
|
+
|
7
|
+
def initialize(message=nil)
|
8
|
+
@message = message || "interrupt"
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
# Interrupt that is thrown whenever a {% break %} is called.
|
13
|
+
class BreakInterrupt < Interrupt; end
|
14
|
+
|
15
|
+
# Interrupt that is thrown whenever a {% continue %} is called.
|
16
|
+
class ContinueInterrupt < Interrupt; end
|
17
|
+
end
|
data/lib/liquid/strainer.rb
CHANGED
@@ -2,24 +2,15 @@ require 'set'
|
|
2
2
|
|
3
3
|
module Liquid
|
4
4
|
|
5
|
-
parent_object = if defined? BlankObject
|
6
|
-
BlankObject
|
7
|
-
else
|
8
|
-
Object
|
9
|
-
end
|
10
|
-
|
11
5
|
# Strainer is the parent class for the filters system.
|
12
|
-
# New filters are mixed into the strainer class which is then
|
6
|
+
# New filters are mixed into the strainer class which is then instantiated for each liquid template render run.
|
13
7
|
#
|
14
|
-
#
|
15
|
-
|
16
|
-
|
17
|
-
@@required_methods = Set.new([:__id__, :__send__, :respond_to?, :kind_of?, :extend, :methods, :singleton_methods, :class, :object_id])
|
18
|
-
|
19
|
-
# Ruby 1.9.2 introduces Object#respond_to_missing?, which is invoked by Object#respond_to?
|
20
|
-
@@required_methods << :respond_to_missing? if Object.respond_to? :respond_to_missing?
|
21
|
-
|
8
|
+
# The Strainer only allows method calls defined in filters given to it via Strainer.global_filter,
|
9
|
+
# Context#add_filters or Template.register_filter
|
10
|
+
class Strainer #:nodoc:
|
22
11
|
@@filters = {}
|
12
|
+
@@known_filters = Set.new
|
13
|
+
@@known_methods = Set.new
|
23
14
|
|
24
15
|
def initialize(context)
|
25
16
|
@context = context
|
@@ -27,28 +18,36 @@ module Liquid
|
|
27
18
|
|
28
19
|
def self.global_filter(filter)
|
29
20
|
raise ArgumentError, "Passed filter is not a module" unless filter.is_a?(Module)
|
21
|
+
add_known_filter(filter)
|
30
22
|
@@filters[filter.name] = filter
|
31
23
|
end
|
32
24
|
|
25
|
+
def self.add_known_filter(filter)
|
26
|
+
unless @@known_filters.include?(filter)
|
27
|
+
@@method_blacklist ||= Set.new(Strainer.instance_methods.map(&:to_s))
|
28
|
+
new_methods = filter.instance_methods.map(&:to_s)
|
29
|
+
new_methods.reject!{ |m| @@method_blacklist.include?(m) }
|
30
|
+
@@known_methods.merge(new_methods)
|
31
|
+
@@known_filters.add(filter)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
33
35
|
def self.create(context)
|
34
36
|
strainer = Strainer.new(context)
|
35
37
|
@@filters.each { |k,m| strainer.extend(m) }
|
36
38
|
strainer
|
37
39
|
end
|
38
40
|
|
39
|
-
def
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
41
|
+
def invoke(method, *args)
|
42
|
+
if invokable?(method)
|
43
|
+
send(method, *args)
|
44
|
+
else
|
45
|
+
args.first
|
46
|
+
end
|
44
47
|
end
|
45
48
|
|
46
|
-
|
47
|
-
|
48
|
-
instance_methods.each do |m|
|
49
|
-
unless @@required_methods.include?(m.to_sym)
|
50
|
-
undef_method m
|
51
|
-
end
|
49
|
+
def invokable?(method)
|
50
|
+
@@known_methods.include?(method.to_s) && respond_to?(method)
|
52
51
|
end
|
53
52
|
end
|
54
53
|
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Liquid
|
2
|
+
|
3
|
+
# Break tag to be used to break out of a for loop.
|
4
|
+
#
|
5
|
+
# == Basic Usage:
|
6
|
+
# {% for item in collection %}
|
7
|
+
# {% if item.condition %}
|
8
|
+
# {% break %}
|
9
|
+
# {% endif %}
|
10
|
+
# {% endfor %}
|
11
|
+
#
|
12
|
+
class Break < Tag
|
13
|
+
|
14
|
+
def interrupt
|
15
|
+
BreakInterrupt.new
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
|
20
|
+
Template.register_tag('break', Break)
|
21
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Liquid
|
2
|
+
|
3
|
+
# Continue tag to be used to break out of a for loop.
|
4
|
+
#
|
5
|
+
# == Basic Usage:
|
6
|
+
# {% for item in collection %}
|
7
|
+
# {% if item.condition %}
|
8
|
+
# {% continue %}
|
9
|
+
# {% endif %}
|
10
|
+
# {% endfor %}
|
11
|
+
#
|
12
|
+
class Continue < Tag
|
13
|
+
|
14
|
+
def interrupt
|
15
|
+
ContinueInterrupt.new
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
|
20
|
+
Template.register_tag('continue', Continue)
|
21
|
+
end
|
data/lib/liquid/tags/for.rb
CHANGED
@@ -69,7 +69,7 @@ module Liquid
|
|
69
69
|
@nodelist = @else_block = []
|
70
70
|
end
|
71
71
|
|
72
|
-
def render(context)
|
72
|
+
def render(context)
|
73
73
|
context.registers[:for] ||= Hash.new(0)
|
74
74
|
|
75
75
|
collection = context[@collection_name]
|
@@ -101,8 +101,8 @@ module Liquid
|
|
101
101
|
# Store our progress through the collection for the continue flag
|
102
102
|
context.registers[:for][@name] = from + segment.length
|
103
103
|
|
104
|
-
context.stack do
|
105
|
-
segment.each_with_index do |item, index|
|
104
|
+
context.stack do
|
105
|
+
segment.each_with_index do |item, index|
|
106
106
|
context[@variable_name] = item
|
107
107
|
context['forloop'] = {
|
108
108
|
'name' => @name,
|
@@ -115,6 +115,13 @@ module Liquid
|
|
115
115
|
'last' => (index == length - 1) }
|
116
116
|
|
117
117
|
result << render_all(@for_block, context)
|
118
|
+
|
119
|
+
# Handle any interrupts if they exist.
|
120
|
+
if context.has_interrupt?
|
121
|
+
interrupt = context.pop_interrupt
|
122
|
+
break if interrupt.is_a? BreakInterrupt
|
123
|
+
next if interrupt.is_a? ContinueInterrupt
|
124
|
+
end
|
118
125
|
end
|
119
126
|
end
|
120
127
|
result
|
data/lib/liquid/variable.rb
CHANGED
@@ -11,7 +11,7 @@ module Liquid
|
|
11
11
|
# {{ user | link }}
|
12
12
|
#
|
13
13
|
class Variable
|
14
|
-
FilterParser = /(?:#{FilterSeparator}|(?:\s*(
|
14
|
+
FilterParser = /(?:#{FilterSeparator}|(?:\s*(?:#{QuotedFragment}|#{ArgumentSeparator})\s*)+)/o
|
15
15
|
attr_accessor :filters, :name
|
16
16
|
|
17
17
|
def initialize(markup)
|
@@ -23,10 +23,10 @@ module Liquid
|
|
23
23
|
if match[2].match(/#{FilterSeparator}\s*(.*)/o)
|
24
24
|
filters = Regexp.last_match(1).scan(FilterParser)
|
25
25
|
filters.each do |f|
|
26
|
-
if matches = f.match(/\s*(\w+)
|
26
|
+
if matches = f.match(/\s*(\w+)(?:\s*#{FilterArgumentSeparator}(.*))?/)
|
27
27
|
filtername = matches[1]
|
28
|
-
filterargs =
|
29
|
-
@filters << [filtername
|
28
|
+
filterargs = matches[2].to_s.scan(/(?:\A|#{ArgumentSeparator})\s*((?:\w+\s*\:\s*)?#{QuotedFragment})/o).flatten
|
29
|
+
@filters << [filtername, filterargs]
|
30
30
|
end
|
31
31
|
end
|
32
32
|
end
|
@@ -36,9 +36,16 @@ module Liquid
|
|
36
36
|
def render(context)
|
37
37
|
return '' if @name.nil?
|
38
38
|
@filters.inject(context[@name]) do |output, filter|
|
39
|
-
filterargs =
|
40
|
-
|
39
|
+
filterargs = []
|
40
|
+
keyword_args = {}
|
41
|
+
filter[1].to_a.each do |a|
|
42
|
+
if matches = a.match(/\A#{TagAttributes}\z/o)
|
43
|
+
keyword_args[matches[1]] = context[matches[2]]
|
44
|
+
else
|
45
|
+
filterargs << context[a]
|
46
|
+
end
|
41
47
|
end
|
48
|
+
filterargs << keyword_args unless keyword_args.empty?
|
42
49
|
begin
|
43
50
|
output = context.invoke(filter[0], output, *filterargs)
|
44
51
|
rescue FilterNotFound
|
data/lib/liquid.rb
CHANGED
data/test/liquid/block_test.rb
CHANGED
data/test/liquid/context_test.rb
CHANGED
@@ -189,10 +189,10 @@ class ContextTest < Test::Unit::TestCase
|
|
189
189
|
end
|
190
190
|
|
191
191
|
context = Context.new
|
192
|
-
|
192
|
+
assert_equal "Wookie", context.invoke("hi", "Wookie")
|
193
|
+
|
193
194
|
context.add_filters(filter)
|
194
|
-
|
195
|
-
assert_equal (methods_before + ["hi"]).sort, methods_after.sort
|
195
|
+
assert_equal "Wookie hi!", context.invoke("hi", "Wookie")
|
196
196
|
end
|
197
197
|
|
198
198
|
def test_add_item_in_outer_scope
|
data/test/liquid/drop_test.rb
CHANGED
@@ -115,6 +115,13 @@ class DropsTest < Test::Unit::TestCase
|
|
115
115
|
assert_equal ' ', output
|
116
116
|
end
|
117
117
|
|
118
|
+
def test_object_methods_not_allowed
|
119
|
+
[:dup, :clone, :singleton_class, :eval, :class_eval, :inspect].each do |method|
|
120
|
+
output = Liquid::Template.parse(" {{ product.#{method} }} ").render('product' => ProductDrop.new)
|
121
|
+
assert_equal ' ', output
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
118
125
|
def test_scope
|
119
126
|
assert_equal '1', Liquid::Template.parse( '{{ context.scopes }}' ).render('context' => ContextDrop.new)
|
120
127
|
assert_equal '2', Liquid::Template.parse( '{%for i in dummy%}{{ context.scopes }}{%endfor%}' ).render('context' => ContextDrop.new, 'dummy' => [1])
|
data/test/liquid/filter_test.rb
CHANGED
@@ -16,6 +16,12 @@ module CanadianMoneyFilter
|
|
16
16
|
end
|
17
17
|
end
|
18
18
|
|
19
|
+
module SubstituteFilter
|
20
|
+
def substitute(input, params={})
|
21
|
+
input.gsub(/%\{(\w+)\}/) { |match| params[$1] }
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
19
25
|
class FiltersTest < Test::Unit::TestCase
|
20
26
|
include Liquid
|
21
27
|
|
@@ -92,6 +98,13 @@ class FiltersTest < Test::Unit::TestCase
|
|
92
98
|
|
93
99
|
assert_equal 1000, Variable.new("var | xyzzy").render(@context)
|
94
100
|
end
|
101
|
+
|
102
|
+
def test_filter_with_keyword_arguments
|
103
|
+
@context['surname'] = 'john'
|
104
|
+
@context.add_filters(SubstituteFilter)
|
105
|
+
output = Variable.new(%! 'hello %{first_name}, %{last_name}' | substitute: first_name: surname, last_name: 'doe' !).render(@context)
|
106
|
+
assert_equal 'hello john, doe', output
|
107
|
+
end
|
95
108
|
end
|
96
109
|
|
97
110
|
class FiltersInTemplate < Test::Unit::TestCase
|
@@ -38,4 +38,27 @@ class SecurityTest < Test::Unit::TestCase
|
|
38
38
|
|
39
39
|
assert_equal expected, Template.parse(text).render(@assigns, :filters => SecurityFilter)
|
40
40
|
end
|
41
|
+
|
42
|
+
def test_does_not_add_filters_to_symbol_table
|
43
|
+
current_symbols = Symbol.all_symbols
|
44
|
+
|
45
|
+
test = %( {{ "some_string" | a_bad_filter }} )
|
46
|
+
|
47
|
+
template = Template.parse(test)
|
48
|
+
assert_equal [], (Symbol.all_symbols - current_symbols)
|
49
|
+
|
50
|
+
template.render
|
51
|
+
assert_equal [], (Symbol.all_symbols - current_symbols)
|
52
|
+
end
|
53
|
+
|
54
|
+
def test_does_not_add_drop_methods_to_symbol_table
|
55
|
+
current_symbols = Symbol.all_symbols
|
56
|
+
|
57
|
+
drop = Drop.new
|
58
|
+
drop.invoke_drop("custom_method_1")
|
59
|
+
drop.invoke_drop("custom_method_2")
|
60
|
+
drop.invoke_drop("custom_method_3")
|
61
|
+
|
62
|
+
assert_equal [], (Symbol.all_symbols - current_symbols)
|
63
|
+
end
|
41
64
|
end # SecurityTest
|
@@ -3,23 +3,50 @@ require 'test_helper'
|
|
3
3
|
class StrainerTest < Test::Unit::TestCase
|
4
4
|
include Liquid
|
5
5
|
|
6
|
+
module AccessScopeFilters
|
7
|
+
def public_filter
|
8
|
+
"public"
|
9
|
+
end
|
10
|
+
|
11
|
+
def private_filter
|
12
|
+
"private"
|
13
|
+
end
|
14
|
+
private :private_filter
|
15
|
+
end
|
16
|
+
|
17
|
+
Strainer.global_filter(AccessScopeFilters)
|
18
|
+
|
6
19
|
def test_strainer
|
7
20
|
strainer = Strainer.create(nil)
|
8
|
-
assert_equal
|
9
|
-
assert_equal
|
10
|
-
|
11
|
-
|
12
|
-
|
21
|
+
assert_equal 5, strainer.invoke('size', 'input')
|
22
|
+
assert_equal "public", strainer.invoke("public_filter")
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_strainer_only_invokes_public_filter_methods
|
26
|
+
strainer = Strainer.create(nil)
|
27
|
+
assert_equal false, strainer.invokable?('__test__')
|
28
|
+
assert_equal false, strainer.invokable?('test')
|
29
|
+
assert_equal false, strainer.invokable?('instance_eval')
|
30
|
+
assert_equal false, strainer.invokable?('__send__')
|
31
|
+
assert_equal true, strainer.invokable?('size') # from the standard lib
|
32
|
+
end
|
33
|
+
|
34
|
+
def test_strainer_returns_nil_if_no_filter_method_found
|
35
|
+
strainer = Strainer.create(nil)
|
36
|
+
assert_nil strainer.invoke("private_filter")
|
37
|
+
assert_nil strainer.invoke("undef_the_filter")
|
13
38
|
end
|
14
39
|
|
15
|
-
def
|
40
|
+
def test_strainer_returns_first_argument_if_no_method_and_arguments_given
|
16
41
|
strainer = Strainer.create(nil)
|
17
|
-
assert_equal
|
42
|
+
assert_equal "password", strainer.invoke("undef_the_method", "password")
|
18
43
|
end
|
19
44
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
assert_equal
|
45
|
+
def test_strainer_only_allows_methods_defined_in_filters
|
46
|
+
strainer = Strainer.create(nil)
|
47
|
+
assert_equal "1 + 1", strainer.invoke("instance_eval", "1 + 1")
|
48
|
+
assert_equal "puts", strainer.invoke("__send__", "puts", "Hi Mom")
|
49
|
+
assert_equal "has_method?", strainer.invoke("invoke", "has_method?", "invoke")
|
24
50
|
end
|
51
|
+
|
25
52
|
end # StrainerTest
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class BreakTagTest < Test::Unit::TestCase
|
4
|
+
include Liquid
|
5
|
+
|
6
|
+
# tests that no weird errors are raised if break is called outside of a
|
7
|
+
# block
|
8
|
+
def test_break_with_no_block
|
9
|
+
assigns = {'i' => 1}
|
10
|
+
markup = '{% break %}'
|
11
|
+
expected = ''
|
12
|
+
|
13
|
+
assert_template_result(expected, markup, assigns)
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class ContinueTagTest < Test::Unit::TestCase
|
4
|
+
include Liquid
|
5
|
+
|
6
|
+
# tests that no weird errors are raised if continue is called outside of a
|
7
|
+
# block
|
8
|
+
def test_continue_with_no_block
|
9
|
+
assigns = {}
|
10
|
+
markup = '{% continue %}'
|
11
|
+
expected = ''
|
12
|
+
|
13
|
+
assert_template_result(expected, markup, assigns)
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
@@ -168,6 +168,88 @@ HERE
|
|
168
168
|
assert_template_result(expected,markup,assigns)
|
169
169
|
end
|
170
170
|
|
171
|
+
def test_for_with_break
|
172
|
+
assigns = {'array' => {'items' => [1,2,3,4,5,6,7,8,9,10]}}
|
173
|
+
|
174
|
+
markup = '{% for i in array.items %}{% break %}{% endfor %}'
|
175
|
+
expected = ""
|
176
|
+
assert_template_result(expected,markup,assigns)
|
177
|
+
|
178
|
+
markup = '{% for i in array.items %}{{ i }}{% break %}{% endfor %}'
|
179
|
+
expected = "1"
|
180
|
+
assert_template_result(expected,markup,assigns)
|
181
|
+
|
182
|
+
markup = '{% for i in array.items %}{% break %}{{ i }}{% endfor %}'
|
183
|
+
expected = ""
|
184
|
+
assert_template_result(expected,markup,assigns)
|
185
|
+
|
186
|
+
markup = '{% for i in array.items %}{{ i }}{% if i > 3 %}{% break %}{% endif %}{% endfor %}'
|
187
|
+
expected = "1234"
|
188
|
+
assert_template_result(expected,markup,assigns)
|
189
|
+
|
190
|
+
# tests to ensure it only breaks out of the local for loop
|
191
|
+
# and not all of them.
|
192
|
+
assigns = {'array' => [[1,2],[3,4],[5,6]] }
|
193
|
+
markup = '{% for item in array %}' +
|
194
|
+
'{% for i in item %}' +
|
195
|
+
'{% if i == 1 %}' +
|
196
|
+
'{% break %}' +
|
197
|
+
'{% endif %}' +
|
198
|
+
'{{ i }}' +
|
199
|
+
'{% endfor %}' +
|
200
|
+
'{% endfor %}'
|
201
|
+
expected = '3456'
|
202
|
+
assert_template_result(expected, markup, assigns)
|
203
|
+
|
204
|
+
# test break does nothing when unreached
|
205
|
+
assigns = {'array' => {'items' => [1,2,3,4,5]}}
|
206
|
+
markup = '{% for i in array.items %}{% if i == 9999 %}{% break %}{% endif %}{{ i }}{% endfor %}'
|
207
|
+
expected = '12345'
|
208
|
+
assert_template_result(expected, markup, assigns)
|
209
|
+
end
|
210
|
+
|
211
|
+
def test_for_with_continue
|
212
|
+
assigns = {'array' => {'items' => [1,2,3,4,5]}}
|
213
|
+
|
214
|
+
markup = '{% for i in array.items %}{% continue %}{% endfor %}'
|
215
|
+
expected = ""
|
216
|
+
assert_template_result(expected,markup,assigns)
|
217
|
+
|
218
|
+
markup = '{% for i in array.items %}{{ i }}{% continue %}{% endfor %}'
|
219
|
+
expected = "12345"
|
220
|
+
assert_template_result(expected,markup,assigns)
|
221
|
+
|
222
|
+
markup = '{% for i in array.items %}{% continue %}{{ i }}{% endfor %}'
|
223
|
+
expected = ""
|
224
|
+
assert_template_result(expected,markup,assigns)
|
225
|
+
|
226
|
+
markup = '{% for i in array.items %}{% if i > 3 %}{% continue %}{% endif %}{{ i }}{% endfor %}'
|
227
|
+
expected = "123"
|
228
|
+
assert_template_result(expected,markup,assigns)
|
229
|
+
|
230
|
+
markup = '{% for i in array.items %}{% if i == 3 %}{% continue %}{% else %}{{ i }}{% endif %}{% endfor %}'
|
231
|
+
expected = "1245"
|
232
|
+
assert_template_result(expected,markup,assigns)
|
233
|
+
|
234
|
+
# tests to ensure it only continues the local for loop and not all of them.
|
235
|
+
assigns = {'array' => [[1,2],[3,4],[5,6]] }
|
236
|
+
markup = '{% for item in array %}' +
|
237
|
+
'{% for i in item %}' +
|
238
|
+
'{% if i == 1 %}' +
|
239
|
+
'{% continue %}' +
|
240
|
+
'{% endif %}' +
|
241
|
+
'{{ i }}' +
|
242
|
+
'{% endfor %}' +
|
243
|
+
'{% endfor %}'
|
244
|
+
expected = '23456'
|
245
|
+
assert_template_result(expected, markup, assigns)
|
246
|
+
|
247
|
+
# test continue does nothing when unreached
|
248
|
+
assigns = {'array' => {'items' => [1,2,3,4,5]}}
|
249
|
+
markup = '{% for i in array.items %}{% if i == 9999 %}{% continue %}{% endif %}{{ i }}{% endfor %}'
|
250
|
+
expected = '12345'
|
251
|
+
assert_template_result(expected, markup, assigns)
|
252
|
+
end
|
171
253
|
|
172
254
|
def test_for_tag_string
|
173
255
|
# ruby 1.8.7 "String".each => Enumerator with single "String" element.
|
@@ -11,67 +11,71 @@ class VariableTest < Test::Unit::TestCase
|
|
11
11
|
def test_filters
|
12
12
|
var = Variable.new('hello | textileze')
|
13
13
|
assert_equal 'hello', var.name
|
14
|
-
assert_equal [[
|
14
|
+
assert_equal [["textileze",[]]], var.filters
|
15
15
|
|
16
16
|
var = Variable.new('hello | textileze | paragraph')
|
17
17
|
assert_equal 'hello', var.name
|
18
|
-
assert_equal [[
|
18
|
+
assert_equal [["textileze",[]], ["paragraph",[]]], var.filters
|
19
19
|
|
20
20
|
var = Variable.new(%! hello | strftime: '%Y'!)
|
21
21
|
assert_equal 'hello', var.name
|
22
|
-
assert_equal [[
|
22
|
+
assert_equal [["strftime",["'%Y'"]]], var.filters
|
23
23
|
|
24
24
|
var = Variable.new(%! 'typo' | link_to: 'Typo', true !)
|
25
25
|
assert_equal %!'typo'!, var.name
|
26
|
-
assert_equal [[
|
26
|
+
assert_equal [["link_to",["'Typo'", "true"]]], var.filters
|
27
27
|
|
28
28
|
var = Variable.new(%! 'typo' | link_to: 'Typo', false !)
|
29
29
|
assert_equal %!'typo'!, var.name
|
30
|
-
assert_equal [[
|
30
|
+
assert_equal [["link_to",["'Typo'", "false"]]], var.filters
|
31
31
|
|
32
32
|
var = Variable.new(%! 'foo' | repeat: 3 !)
|
33
33
|
assert_equal %!'foo'!, var.name
|
34
|
-
assert_equal [[
|
34
|
+
assert_equal [["repeat",["3"]]], var.filters
|
35
35
|
|
36
36
|
var = Variable.new(%! 'foo' | repeat: 3, 3 !)
|
37
37
|
assert_equal %!'foo'!, var.name
|
38
|
-
assert_equal [[
|
38
|
+
assert_equal [["repeat",["3","3"]]], var.filters
|
39
39
|
|
40
40
|
var = Variable.new(%! 'foo' | repeat: 3, 3, 3 !)
|
41
41
|
assert_equal %!'foo'!, var.name
|
42
|
-
assert_equal [[
|
42
|
+
assert_equal [["repeat",["3","3","3"]]], var.filters
|
43
43
|
|
44
44
|
var = Variable.new(%! hello | strftime: '%Y, okay?'!)
|
45
45
|
assert_equal 'hello', var.name
|
46
|
-
assert_equal [[
|
46
|
+
assert_equal [["strftime",["'%Y, okay?'"]]], var.filters
|
47
47
|
|
48
48
|
var = Variable.new(%! hello | things: "%Y, okay?", 'the other one'!)
|
49
49
|
assert_equal 'hello', var.name
|
50
|
-
assert_equal [[
|
50
|
+
assert_equal [["things",["\"%Y, okay?\"","'the other one'"]]], var.filters
|
51
51
|
end
|
52
52
|
|
53
53
|
def test_filter_with_date_parameter
|
54
54
|
|
55
55
|
var = Variable.new(%! '2006-06-06' | date: "%m/%d/%Y"!)
|
56
56
|
assert_equal "'2006-06-06'", var.name
|
57
|
-
assert_equal [[
|
57
|
+
assert_equal [["date",["\"%m/%d/%Y\""]]], var.filters
|
58
58
|
|
59
59
|
end
|
60
60
|
|
61
61
|
def test_filters_without_whitespace
|
62
62
|
var = Variable.new('hello | textileze | paragraph')
|
63
63
|
assert_equal 'hello', var.name
|
64
|
-
assert_equal [[
|
64
|
+
assert_equal [["textileze",[]], ["paragraph",[]]], var.filters
|
65
65
|
|
66
66
|
var = Variable.new('hello|textileze|paragraph')
|
67
67
|
assert_equal 'hello', var.name
|
68
|
-
assert_equal [[
|
68
|
+
assert_equal [["textileze",[]], ["paragraph",[]]], var.filters
|
69
|
+
|
70
|
+
var = Variable.new("hello|replace:'foo','bar'|textileze")
|
71
|
+
assert_equal 'hello', var.name
|
72
|
+
assert_equal [["replace", ["'foo'", "'bar'"]], ["textileze", []]], var.filters
|
69
73
|
end
|
70
74
|
|
71
75
|
def test_symbol
|
72
76
|
var = Variable.new("http://disney.com/logo.gif | image: 'med' ")
|
73
77
|
assert_equal 'http://disney.com/logo.gif', var.name
|
74
|
-
assert_equal [[
|
78
|
+
assert_equal [["image",["'med'"]]], var.filters
|
75
79
|
end
|
76
80
|
|
77
81
|
def test_string_single_quoted
|
@@ -103,6 +107,12 @@ class VariableTest < Test::Unit::TestCase
|
|
103
107
|
var = Variable.new(%| test.test |)
|
104
108
|
assert_equal 'test.test', var.name
|
105
109
|
end
|
110
|
+
|
111
|
+
def test_filter_with_keyword_arguments
|
112
|
+
var = Variable.new(%! hello | things: greeting: "world", farewell: 'goodbye'!)
|
113
|
+
assert_equal 'hello', var.name
|
114
|
+
assert_equal [['things',["greeting: \"world\"","farewell: 'goodbye'"]]], var.filters
|
115
|
+
end
|
106
116
|
end
|
107
117
|
|
108
118
|
|
metadata
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: liquid
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.4.1
|
5
4
|
prerelease:
|
5
|
+
version: 2.5.0
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
8
8
|
- Tobias Luetke
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2013-03-06 00:00:00.000000000 Z
|
13
13
|
dependencies: []
|
14
14
|
description:
|
15
15
|
email:
|
@@ -30,14 +30,17 @@ files:
|
|
30
30
|
- lib/liquid/extensions.rb
|
31
31
|
- lib/liquid/file_system.rb
|
32
32
|
- lib/liquid/htmltags.rb
|
33
|
+
- lib/liquid/interrupts.rb
|
33
34
|
- lib/liquid/module_ex.rb
|
34
35
|
- lib/liquid/standardfilters.rb
|
35
36
|
- lib/liquid/strainer.rb
|
36
37
|
- lib/liquid/tag.rb
|
37
38
|
- lib/liquid/tags/assign.rb
|
39
|
+
- lib/liquid/tags/break.rb
|
38
40
|
- lib/liquid/tags/capture.rb
|
39
41
|
- lib/liquid/tags/case.rb
|
40
42
|
- lib/liquid/tags/comment.rb
|
43
|
+
- lib/liquid/tags/continue.rb
|
41
44
|
- lib/liquid/tags/cycle.rb
|
42
45
|
- lib/liquid/tags/decrement.rb
|
43
46
|
- lib/liquid/tags/for.rb
|
@@ -53,6 +56,7 @@ files:
|
|
53
56
|
- lib/liquid.rb
|
54
57
|
- MIT-LICENSE
|
55
58
|
- README.md
|
59
|
+
- History.md
|
56
60
|
- test/liquid/assign_test.rb
|
57
61
|
- test/liquid/block_test.rb
|
58
62
|
- test/liquid/capture_test.rb
|
@@ -69,6 +73,8 @@ files:
|
|
69
73
|
- test/liquid/security_test.rb
|
70
74
|
- test/liquid/standard_filter_test.rb
|
71
75
|
- test/liquid/strainer_test.rb
|
76
|
+
- test/liquid/tags/break_tag_test.rb
|
77
|
+
- test/liquid/tags/continue_tag_test.rb
|
72
78
|
- test/liquid/tags/for_tag_test.rb
|
73
79
|
- test/liquid/tags/html_tag_test.rb
|
74
80
|
- test/liquid/tags/if_else_tag_test.rb
|
@@ -81,7 +87,6 @@ files:
|
|
81
87
|
- test/liquid/template_test.rb
|
82
88
|
- test/liquid/variable_test.rb
|
83
89
|
- test/test_helper.rb
|
84
|
-
- History.md
|
85
90
|
homepage: http://www.liquidmarkup.org
|
86
91
|
licenses: []
|
87
92
|
post_install_message:
|
@@ -89,20 +94,20 @@ rdoc_options: []
|
|
89
94
|
require_paths:
|
90
95
|
- lib
|
91
96
|
required_ruby_version: !ruby/object:Gem::Requirement
|
92
|
-
none: false
|
93
97
|
requirements:
|
94
98
|
- - ! '>='
|
95
99
|
- !ruby/object:Gem::Version
|
96
100
|
version: '0'
|
97
|
-
required_rubygems_version: !ruby/object:Gem::Requirement
|
98
101
|
none: false
|
102
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
99
103
|
requirements:
|
100
104
|
- - ! '>='
|
101
105
|
- !ruby/object:Gem::Version
|
102
106
|
version: 1.3.7
|
107
|
+
none: false
|
103
108
|
requirements: []
|
104
109
|
rubyforge_project:
|
105
|
-
rubygems_version: 1.8.
|
110
|
+
rubygems_version: 1.8.23
|
106
111
|
signing_key:
|
107
112
|
specification_version: 3
|
108
113
|
summary: A secure, non-evaling end user template engine with aesthetic markup.
|
@@ -123,6 +128,8 @@ test_files:
|
|
123
128
|
- test/liquid/security_test.rb
|
124
129
|
- test/liquid/standard_filter_test.rb
|
125
130
|
- test/liquid/strainer_test.rb
|
131
|
+
- test/liquid/tags/break_tag_test.rb
|
132
|
+
- test/liquid/tags/continue_tag_test.rb
|
126
133
|
- test/liquid/tags/for_tag_test.rb
|
127
134
|
- test/liquid/tags/html_tag_test.rb
|
128
135
|
- test/liquid/tags/if_else_tag_test.rb
|