liquid 2.4.1 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|