spinto-liquid 2.3.0.1
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 +56 -0
- data/MIT-LICENSE +20 -0
- data/README.md +44 -0
- data/lib/extras/liquid_view.rb +51 -0
- data/lib/liquid/block.rb +101 -0
- data/lib/liquid/condition.rb +120 -0
- data/lib/liquid/context.rb +245 -0
- data/lib/liquid/document.rb +17 -0
- data/lib/liquid/drop.rb +49 -0
- data/lib/liquid/errors.rb +11 -0
- data/lib/liquid/extensions.rb +62 -0
- data/lib/liquid/file_system.rb +62 -0
- data/lib/liquid/htmltags.rb +75 -0
- data/lib/liquid/module_ex.rb +62 -0
- data/lib/liquid/standardfilters.rb +241 -0
- data/lib/liquid/strainer.rb +54 -0
- data/lib/liquid/tag.rb +26 -0
- data/lib/liquid/tags/assign.rb +33 -0
- data/lib/liquid/tags/capture.rb +35 -0
- data/lib/liquid/tags/case.rb +79 -0
- data/lib/liquid/tags/comment.rb +9 -0
- data/lib/liquid/tags/cycle.rb +59 -0
- data/lib/liquid/tags/decrement.rb +39 -0
- data/lib/liquid/tags/for.rb +190 -0
- data/lib/liquid/tags/if.rb +79 -0
- data/lib/liquid/tags/ifchanged.rb +20 -0
- data/lib/liquid/tags/include.rb +65 -0
- data/lib/liquid/tags/increment.rb +35 -0
- data/lib/liquid/tags/raw.rb +21 -0
- data/lib/liquid/tags/unless.rb +33 -0
- data/lib/liquid/template.rb +150 -0
- data/lib/liquid/variable.rb +50 -0
- data/lib/liquid.rb +66 -0
- data/test/liquid/assign_test.rb +21 -0
- data/test/liquid/block_test.rb +58 -0
- data/test/liquid/capture_test.rb +40 -0
- data/test/liquid/condition_test.rb +127 -0
- data/test/liquid/context_test.rb +478 -0
- data/test/liquid/drop_test.rb +162 -0
- data/test/liquid/error_handling_test.rb +81 -0
- data/test/liquid/file_system_test.rb +29 -0
- data/test/liquid/filter_test.rb +106 -0
- data/test/liquid/module_ex_test.rb +87 -0
- data/test/liquid/output_test.rb +116 -0
- data/test/liquid/parsing_quirks_test.rb +52 -0
- data/test/liquid/regexp_test.rb +44 -0
- data/test/liquid/security_test.rb +41 -0
- data/test/liquid/standard_filter_test.rb +195 -0
- data/test/liquid/strainer_test.rb +25 -0
- data/test/liquid/tags/for_tag_test.rb +215 -0
- data/test/liquid/tags/html_tag_test.rb +39 -0
- data/test/liquid/tags/if_else_tag_test.rb +160 -0
- data/test/liquid/tags/include_tag_test.rb +139 -0
- data/test/liquid/tags/increment_tag_test.rb +24 -0
- data/test/liquid/tags/raw_tag_test.rb +15 -0
- data/test/liquid/tags/standard_tag_test.rb +295 -0
- data/test/liquid/tags/statements_test.rb +134 -0
- data/test/liquid/tags/unless_else_tag_test.rb +26 -0
- data/test/liquid/template_test.rb +74 -0
- data/test/liquid/variable_test.rb +170 -0
- data/test/test_helper.rb +29 -0
- metadata +136 -0
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
module Liquid
|
4
|
+
|
5
|
+
parent_object = if defined? BlankObject
|
6
|
+
BlankObject
|
7
|
+
else
|
8
|
+
Object
|
9
|
+
end
|
10
|
+
|
11
|
+
# Strainer is the parent class for the filters system.
|
12
|
+
# New filters are mixed into the strainer class which is then instanciated for each liquid template render run.
|
13
|
+
#
|
14
|
+
# One of the strainer's responsibilities is to keep malicious method calls out
|
15
|
+
class Strainer < parent_object #:nodoc:
|
16
|
+
INTERNAL_METHOD = /^__/
|
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
|
+
|
22
|
+
@@filters = {}
|
23
|
+
|
24
|
+
def initialize(context)
|
25
|
+
@context = context
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.global_filter(filter)
|
29
|
+
raise ArgumentError, "Passed filter is not a module" unless filter.is_a?(Module)
|
30
|
+
@@filters[filter.name] = filter
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.create(context)
|
34
|
+
strainer = Strainer.new(context)
|
35
|
+
@@filters.each { |k,m| strainer.extend(m) }
|
36
|
+
strainer
|
37
|
+
end
|
38
|
+
|
39
|
+
def respond_to?(method, include_private = false)
|
40
|
+
method_name = method.to_s
|
41
|
+
return false if method_name =~ INTERNAL_METHOD
|
42
|
+
return false if @@required_methods.include?(method_name)
|
43
|
+
super
|
44
|
+
end
|
45
|
+
|
46
|
+
# remove all standard methods from the bucket so circumvent security
|
47
|
+
# problems
|
48
|
+
instance_methods.each do |m|
|
49
|
+
unless @@required_methods.include?(m.to_sym)
|
50
|
+
undef_method m
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
data/lib/liquid/tag.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
module Liquid
|
2
|
+
|
3
|
+
class Tag
|
4
|
+
|
5
|
+
attr_accessor :nodelist
|
6
|
+
|
7
|
+
def initialize(tag_name, markup, tokens)
|
8
|
+
@tag_name = tag_name
|
9
|
+
@markup = markup
|
10
|
+
parse(tokens)
|
11
|
+
end
|
12
|
+
|
13
|
+
def parse(tokens)
|
14
|
+
end
|
15
|
+
|
16
|
+
def name
|
17
|
+
self.class.name.downcase
|
18
|
+
end
|
19
|
+
|
20
|
+
def render(context)
|
21
|
+
''
|
22
|
+
end
|
23
|
+
|
24
|
+
end # Tag
|
25
|
+
|
26
|
+
end # Tag
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Liquid
|
2
|
+
|
3
|
+
# Assign sets a variable in your template.
|
4
|
+
#
|
5
|
+
# {% assign foo = 'monkey' %}
|
6
|
+
#
|
7
|
+
# You can then use the variable later in the page.
|
8
|
+
#
|
9
|
+
# {{ foo }}
|
10
|
+
#
|
11
|
+
class Assign < Tag
|
12
|
+
Syntax = /(#{VariableSignature}+)\s*=\s*(.*)\s*/
|
13
|
+
|
14
|
+
def initialize(tag_name, markup, tokens)
|
15
|
+
if markup =~ Syntax
|
16
|
+
@to = $1
|
17
|
+
@from = Variable.new($2)
|
18
|
+
else
|
19
|
+
raise SyntaxError.new("Syntax Error in 'assign' - Valid syntax: assign [var] = [source]")
|
20
|
+
end
|
21
|
+
|
22
|
+
super
|
23
|
+
end
|
24
|
+
|
25
|
+
def render(context)
|
26
|
+
context.scopes.last[@to] = @from.render(context)
|
27
|
+
''
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
Template.register_tag('assign', Assign)
|
33
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Liquid
|
2
|
+
|
3
|
+
# Capture stores the result of a block into a variable without rendering it inplace.
|
4
|
+
#
|
5
|
+
# {% capture heading %}
|
6
|
+
# Monkeys!
|
7
|
+
# {% endcapture %}
|
8
|
+
# ...
|
9
|
+
# <h1>{{ heading }}</h1>
|
10
|
+
#
|
11
|
+
# Capture is useful for saving content for use later in your template, such as
|
12
|
+
# in a sidebar or footer.
|
13
|
+
#
|
14
|
+
class Capture < Block
|
15
|
+
Syntax = /(\w+)/
|
16
|
+
|
17
|
+
def initialize(tag_name, markup, tokens)
|
18
|
+
if markup =~ Syntax
|
19
|
+
@to = $1
|
20
|
+
else
|
21
|
+
raise SyntaxError.new("Syntax Error in 'capture' - Valid syntax: capture [var]")
|
22
|
+
end
|
23
|
+
|
24
|
+
super
|
25
|
+
end
|
26
|
+
|
27
|
+
def render(context)
|
28
|
+
output = super
|
29
|
+
context.scopes.last[@to] = output
|
30
|
+
''
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
Template.register_tag('capture', Capture)
|
35
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
module Liquid
|
2
|
+
class Case < Block
|
3
|
+
Syntax = /(#{QuotedFragment})/
|
4
|
+
WhenSyntax = /(#{QuotedFragment})(?:(?:\s+or\s+|\s*\,\s*)(#{QuotedFragment}.*))?/
|
5
|
+
|
6
|
+
def initialize(tag_name, markup, tokens)
|
7
|
+
@blocks = []
|
8
|
+
|
9
|
+
if markup =~ Syntax
|
10
|
+
@left = $1
|
11
|
+
else
|
12
|
+
raise SyntaxError.new("Syntax Error in tag 'case' - Valid syntax: case [condition]")
|
13
|
+
end
|
14
|
+
|
15
|
+
super
|
16
|
+
end
|
17
|
+
|
18
|
+
def unknown_tag(tag, markup, tokens)
|
19
|
+
@nodelist = []
|
20
|
+
case tag
|
21
|
+
when 'when'
|
22
|
+
record_when_condition(markup)
|
23
|
+
when 'else'
|
24
|
+
record_else_condition(markup)
|
25
|
+
else
|
26
|
+
super
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def render(context)
|
31
|
+
context.stack do
|
32
|
+
execute_else_block = true
|
33
|
+
|
34
|
+
output = ''
|
35
|
+
@blocks.each do |block|
|
36
|
+
if block.else?
|
37
|
+
return render_all(block.attachment, context) if execute_else_block
|
38
|
+
elsif block.evaluate(context)
|
39
|
+
execute_else_block = false
|
40
|
+
output << render_all(block.attachment, context)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
output
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def record_when_condition(markup)
|
50
|
+
while markup
|
51
|
+
# Create a new nodelist and assign it to the new block
|
52
|
+
if not markup =~ WhenSyntax
|
53
|
+
raise SyntaxError.new("Syntax Error in tag 'case' - Valid when condition: {% when [condition] [or condition2...] %} ")
|
54
|
+
end
|
55
|
+
|
56
|
+
markup = $2
|
57
|
+
|
58
|
+
block = Condition.new(@left, '==', $1)
|
59
|
+
block.attach(@nodelist)
|
60
|
+
@blocks.push(block)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def record_else_condition(markup)
|
65
|
+
|
66
|
+
if not markup.strip.empty?
|
67
|
+
raise SyntaxError.new("Syntax Error in tag 'case' - Valid else condition: {% else %} (no parameters) ")
|
68
|
+
end
|
69
|
+
|
70
|
+
block = ElseCondition.new
|
71
|
+
block.attach(@nodelist)
|
72
|
+
@blocks << block
|
73
|
+
end
|
74
|
+
|
75
|
+
|
76
|
+
end
|
77
|
+
|
78
|
+
Template.register_tag('case', Case)
|
79
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module Liquid
|
2
|
+
|
3
|
+
# Cycle is usually used within a loop to alternate between values, like colors or DOM classes.
|
4
|
+
#
|
5
|
+
# {% for item in items %}
|
6
|
+
# <div class="{% cycle 'red', 'green', 'blue' %}"> {{ item }} </div>
|
7
|
+
# {% end %}
|
8
|
+
#
|
9
|
+
# <div class="red"> Item one </div>
|
10
|
+
# <div class="green"> Item two </div>
|
11
|
+
# <div class="blue"> Item three </div>
|
12
|
+
# <div class="red"> Item four </div>
|
13
|
+
# <div class="green"> Item five</div>
|
14
|
+
#
|
15
|
+
class Cycle < Tag
|
16
|
+
SimpleSyntax = /^#{QuotedFragment}+/
|
17
|
+
NamedSyntax = /^(#{QuotedFragment})\s*\:\s*(.*)/
|
18
|
+
|
19
|
+
def initialize(tag_name, markup, tokens)
|
20
|
+
case markup
|
21
|
+
when NamedSyntax
|
22
|
+
@variables = variables_from_string($2)
|
23
|
+
@name = $1
|
24
|
+
when SimpleSyntax
|
25
|
+
@variables = variables_from_string(markup)
|
26
|
+
@name = "'#{@variables.to_s}'"
|
27
|
+
else
|
28
|
+
raise SyntaxError.new("Syntax Error in 'cycle' - Valid syntax: cycle [name :] var [, var2, var3 ...]")
|
29
|
+
end
|
30
|
+
super
|
31
|
+
end
|
32
|
+
|
33
|
+
def render(context)
|
34
|
+
context.registers[:cycle] ||= Hash.new(0)
|
35
|
+
|
36
|
+
context.stack do
|
37
|
+
key = context[@name]
|
38
|
+
iteration = context.registers[:cycle][key]
|
39
|
+
result = context[@variables[iteration]]
|
40
|
+
iteration += 1
|
41
|
+
iteration = 0 if iteration >= @variables.size
|
42
|
+
context.registers[:cycle][key] = iteration
|
43
|
+
result
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def variables_from_string(markup)
|
50
|
+
markup.split(',').collect do |var|
|
51
|
+
var =~ /\s*(#{QuotedFragment})\s*/
|
52
|
+
$1 ? $1 : nil
|
53
|
+
end.compact
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
|
58
|
+
Template.register_tag('cycle', Cycle)
|
59
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Liquid
|
2
|
+
|
3
|
+
# decrement is used in a place where one needs to insert a counter
|
4
|
+
# into a template, and needs the counter to survive across
|
5
|
+
# multiple instantiations of the template.
|
6
|
+
# NOTE: decrement is a pre-decrement, --i,
|
7
|
+
# while increment is post: i++.
|
8
|
+
#
|
9
|
+
# (To achieve the survival, the application must keep the context)
|
10
|
+
#
|
11
|
+
# if the variable does not exist, it is created with value 0.
|
12
|
+
|
13
|
+
# Hello: {% decrement variable %}
|
14
|
+
#
|
15
|
+
# gives you:
|
16
|
+
#
|
17
|
+
# Hello: -1
|
18
|
+
# Hello: -2
|
19
|
+
# Hello: -3
|
20
|
+
#
|
21
|
+
class Decrement < Tag
|
22
|
+
def initialize(tag_name, markup, tokens)
|
23
|
+
@variable = markup.strip
|
24
|
+
|
25
|
+
super
|
26
|
+
end
|
27
|
+
|
28
|
+
def render(context)
|
29
|
+
value = context.environments.first[@variable] ||= 0
|
30
|
+
value = value - 1
|
31
|
+
context.environments.first[@variable] = value
|
32
|
+
value.to_s
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
end
|
37
|
+
|
38
|
+
Template.register_tag('decrement', Decrement)
|
39
|
+
end
|
@@ -0,0 +1,190 @@
|
|
1
|
+
module Liquid
|
2
|
+
|
3
|
+
# "For" iterates over an array or collection.
|
4
|
+
# Several useful variables are available to you within the loop.
|
5
|
+
#
|
6
|
+
# == Basic usage:
|
7
|
+
# {% for item in collection %}
|
8
|
+
# {{ forloop.index }}: {{ item.name }}
|
9
|
+
# {% endfor %}
|
10
|
+
#
|
11
|
+
# == Advanced usage:
|
12
|
+
# {% for item in collection %}
|
13
|
+
# <div {% if forloop.first %}class="first"{% endif %}>
|
14
|
+
# Item {{ forloop.index }}: {{ item.name }}
|
15
|
+
# </div>
|
16
|
+
# {% else %}
|
17
|
+
# There is nothing in the collection.
|
18
|
+
# {% endfor %}
|
19
|
+
#
|
20
|
+
# You can also define a limit and offset much like SQL. Remember
|
21
|
+
# that offset starts at 0 for the first item.
|
22
|
+
#
|
23
|
+
# {% for item in collection limit:5 offset:10 %}
|
24
|
+
# {{ item.name }}
|
25
|
+
# {% end %}
|
26
|
+
#
|
27
|
+
# You can also specify an order for the collection items
|
28
|
+
#
|
29
|
+
# {% for item in collection order:ascending %}
|
30
|
+
# {{ item.name }}
|
31
|
+
# {% end %}
|
32
|
+
#
|
33
|
+
# You can also specify which attribute to sort by.
|
34
|
+
#
|
35
|
+
# {% for item in collection sort_by:name order:descending %}
|
36
|
+
# {{ item.name }}
|
37
|
+
# {% end %}
|
38
|
+
#
|
39
|
+
# To reverse the for loop simply use {% for item in collection reversed %}
|
40
|
+
#
|
41
|
+
# == Available variables:
|
42
|
+
#
|
43
|
+
# forloop.name:: 'item-collection'
|
44
|
+
# forloop.length:: Length of the loop
|
45
|
+
# forloop.index:: The current item's position in the collection;
|
46
|
+
# forloop.index starts at 1.
|
47
|
+
# This is helpful for non-programmers who start believe
|
48
|
+
# the first item in an array is 1, not 0.
|
49
|
+
# forloop.index0:: The current item's position in the collection
|
50
|
+
# where the first item is 0
|
51
|
+
# forloop.rindex:: Number of items remaining in the loop
|
52
|
+
# (length - index) where 1 is the last item.
|
53
|
+
# forloop.rindex0:: Number of items remaining in the loop
|
54
|
+
# where 0 is the last item.
|
55
|
+
# forloop.first:: Returns true if the item is the first item.
|
56
|
+
# forloop.last:: Returns true if the item is the last item.
|
57
|
+
#
|
58
|
+
class For < Block
|
59
|
+
Syntax = /(\w+)\s+in\s+(#{QuotedFragment}+)\s*(reversed)?/
|
60
|
+
|
61
|
+
def initialize(tag_name, markup, tokens)
|
62
|
+
if markup =~ Syntax
|
63
|
+
@variable_name = $1
|
64
|
+
@collection_name = $2
|
65
|
+
@name = "#{$1}-#{$2}"
|
66
|
+
@reversed = $3
|
67
|
+
@attributes = {}
|
68
|
+
markup.scan(TagAttributes) do |key, value|
|
69
|
+
@attributes[key] = value
|
70
|
+
end
|
71
|
+
@reversed = 'reversed' if @attributes['order'] == 'descending'
|
72
|
+
else
|
73
|
+
raise SyntaxError.new("Syntax Error in 'for loop' - Valid syntax: for [item] in [collection]")
|
74
|
+
end
|
75
|
+
|
76
|
+
@nodelist = @for_block = []
|
77
|
+
super
|
78
|
+
end
|
79
|
+
|
80
|
+
def unknown_tag(tag, markup, tokens)
|
81
|
+
return super unless tag == 'else'
|
82
|
+
@nodelist = @else_block = []
|
83
|
+
end
|
84
|
+
|
85
|
+
def render(context)
|
86
|
+
context.registers[:for] ||= Hash.new(0)
|
87
|
+
|
88
|
+
collection = context[@collection_name]
|
89
|
+
collection = collection.to_a if collection.is_a?(Range)
|
90
|
+
|
91
|
+
# Maintains Ruby 1.8.7 String#each behaviour on 1.9
|
92
|
+
return render_else(context) unless iterable?(collection)
|
93
|
+
|
94
|
+
sort_property = @attributes['sort_by']
|
95
|
+
order_property = @attributes['order']
|
96
|
+
if sort_property || order_property
|
97
|
+
collection = if sort_property.nil? && (@attributes['order'] == 'ascending' || @attributes['order'] == 'descending')
|
98
|
+
collection.sort
|
99
|
+
elsif collection.first.respond_to?('[]') and !collection.first[sort_property].nil?
|
100
|
+
collection.sort {|a,b| a[sort_property] <=> b[sort_property] }
|
101
|
+
elsif collection.first.respond_to?(sort_property)
|
102
|
+
collection.sort {|a,b| a.send(sort_property) <=> b.send(sort_property) }
|
103
|
+
else
|
104
|
+
collection
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
collection.reverse! if @reversed
|
109
|
+
|
110
|
+
from = if @attributes['offset'] == 'continue'
|
111
|
+
context.registers[:for][@name].to_i
|
112
|
+
else
|
113
|
+
context[@attributes['offset']].to_i
|
114
|
+
end
|
115
|
+
|
116
|
+
limit = context[@attributes['limit']]
|
117
|
+
to = limit ? limit.to_i + from : nil
|
118
|
+
|
119
|
+
|
120
|
+
segment = slice_collection_using_each(collection, from, to)
|
121
|
+
|
122
|
+
return render_else(context) if segment.empty?
|
123
|
+
|
124
|
+
result = ''
|
125
|
+
|
126
|
+
length = segment.length
|
127
|
+
|
128
|
+
# Store our progress through the collection for the continue flag
|
129
|
+
context.registers[:for][@name] = from + segment.length
|
130
|
+
|
131
|
+
context.stack do
|
132
|
+
segment.each_with_index do |item, index|
|
133
|
+
context[@variable_name] = item
|
134
|
+
context['forloop'] = {
|
135
|
+
'name' => @name,
|
136
|
+
'length' => length,
|
137
|
+
'index' => index + 1,
|
138
|
+
'index0' => index,
|
139
|
+
'rindex' => length - index,
|
140
|
+
'rindex0' => length - index -1,
|
141
|
+
'first' => (index == 0),
|
142
|
+
'last' => (index == length - 1) }
|
143
|
+
|
144
|
+
result << render_all(@for_block, context)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
result
|
148
|
+
end
|
149
|
+
|
150
|
+
def slice_collection_using_each(collection, from, to)
|
151
|
+
segments = []
|
152
|
+
index = 0
|
153
|
+
yielded = 0
|
154
|
+
|
155
|
+
# Maintains Ruby 1.8.7 String#each behaviour on 1.9
|
156
|
+
return [collection] if non_blank_string?(collection)
|
157
|
+
|
158
|
+
collection.each do |item|
|
159
|
+
|
160
|
+
if to && to <= index
|
161
|
+
break
|
162
|
+
end
|
163
|
+
|
164
|
+
if from <= index
|
165
|
+
segments << item
|
166
|
+
end
|
167
|
+
|
168
|
+
index += 1
|
169
|
+
end
|
170
|
+
|
171
|
+
segments
|
172
|
+
end
|
173
|
+
|
174
|
+
private
|
175
|
+
|
176
|
+
def render_else(context)
|
177
|
+
return @else_block ? [render_all(@else_block, context)] : ''
|
178
|
+
end
|
179
|
+
|
180
|
+
def iterable?(collection)
|
181
|
+
collection.respond_to?(:each) || non_blank_string?(collection)
|
182
|
+
end
|
183
|
+
|
184
|
+
def non_blank_string?(collection)
|
185
|
+
collection.is_a?(String) && collection != ''
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
Template.register_tag('for', For)
|
190
|
+
end
|