liquid 1.7.0 → 1.9.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/CHANGELOG +17 -15
- data/History.txt +44 -0
- data/MIT-LICENSE +2 -2
- data/Manifest.txt +6 -1
- data/{README → README.txt} +0 -0
- data/Rakefile +3 -3
- data/init.rb +5 -3
- data/lib/liquid.rb +8 -6
- data/lib/liquid/block.rb +6 -9
- data/lib/liquid/condition.rb +49 -17
- data/lib/liquid/context.rb +67 -41
- data/lib/liquid/errors.rb +8 -5
- data/lib/liquid/htmltags.rb +17 -7
- data/lib/liquid/module_ex.rb +62 -0
- data/lib/liquid/standardfilters.rb +39 -0
- data/lib/liquid/strainer.rb +20 -11
- data/lib/liquid/tag.rb +4 -3
- data/lib/liquid/tags/assign.rb +15 -4
- data/lib/liquid/tags/capture.rb +15 -2
- data/lib/liquid/tags/case.rb +51 -36
- data/lib/liquid/tags/cycle.rb +16 -2
- data/lib/liquid/tags/for.rb +45 -8
- data/lib/liquid/tags/if.rb +35 -7
- data/lib/liquid/tags/include.rb +2 -3
- data/lib/liquid/tags/unless.rb +6 -2
- data/lib/liquid/template.rb +13 -18
- data/lib/liquid/variable.rb +25 -12
- data/test/block_test.rb +8 -0
- data/test/condition_test.rb +109 -0
- data/test/context_test.rb +88 -10
- data/test/drop_test.rb +3 -1
- data/test/error_handling_test.rb +16 -3
- data/test/extra/breakpoint.rb +0 -0
- data/test/extra/caller.rb +0 -0
- data/test/filter_test.rb +3 -3
- data/test/html_tag_test.rb +7 -0
- data/test/if_else_test.rb +32 -0
- data/test/include_tag_test.rb +24 -1
- data/test/module_ex_test.rb +89 -0
- data/test/parsing_quirks_test.rb +15 -0
- data/test/regexp_test.rb +4 -3
- data/test/standard_filter_test.rb +27 -2
- data/test/standard_tag_test.rb +67 -20
- data/test/test_helper.rb +20 -0
- data/test/unless_else_test.rb +8 -0
- metadata +60 -46
data/lib/liquid/tags/cycle.rb
CHANGED
@@ -1,9 +1,22 @@
|
|
1
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
|
+
#
|
2
15
|
class Cycle < Tag
|
3
16
|
SimpleSyntax = /#{QuotedFragment}/
|
4
17
|
NamedSyntax = /(#{QuotedFragment})\s*\:\s*(.*)/
|
5
18
|
|
6
|
-
def initialize(markup, tokens)
|
19
|
+
def initialize(tag_name, markup, tokens)
|
7
20
|
case markup
|
8
21
|
when NamedSyntax
|
9
22
|
@variables = variables_from_string($2)
|
@@ -14,7 +27,8 @@ module Liquid
|
|
14
27
|
else
|
15
28
|
raise SyntaxError.new("Syntax Error in 'cycle' - Valid syntax: cycle [name :] var [, var2, var3 ...]")
|
16
29
|
end
|
17
|
-
|
30
|
+
|
31
|
+
super
|
18
32
|
end
|
19
33
|
|
20
34
|
def render(context)
|
data/lib/liquid/tags/for.rb
CHANGED
@@ -1,10 +1,48 @@
|
|
1
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
|
+
# {% endfor %}
|
17
|
+
#
|
18
|
+
# You can also define a limit and offset much like SQL. Remember
|
19
|
+
# that offset starts at 0 for the first item.
|
20
|
+
#
|
21
|
+
# {% for item in collection limit:5 offset:10 %}
|
22
|
+
# {{ item.name }}
|
23
|
+
# {% end %}
|
24
|
+
#
|
25
|
+
# == Available variables:
|
26
|
+
#
|
27
|
+
# forloop.name:: 'item-collection'
|
28
|
+
# forloop.length:: Length of the loop
|
29
|
+
# forloop.index:: The current item's position in the collection;
|
30
|
+
# forloop.index starts at 1.
|
31
|
+
# This is helpful for non-programmers who start believe
|
32
|
+
# the first item in an array is 1, not 0.
|
33
|
+
# forloop.index0:: The current item's position in the collection
|
34
|
+
# where the first item is 0
|
35
|
+
# forloop.rindex:: Number of items remaining in the loop
|
36
|
+
# (length - index) where 1 is the last item.
|
37
|
+
# forloop.rindex0:: Number of items remaining in the loop
|
38
|
+
# where 0 is the last item.
|
39
|
+
# forloop.first:: Returns true if the item is the first item.
|
40
|
+
# forloop.last:: Returns true if the item is the last item.
|
41
|
+
#
|
2
42
|
class For < Block
|
3
43
|
Syntax = /(\w+)\s+in\s+(#{VariableSignature}+)/
|
4
44
|
|
5
|
-
def initialize(markup, tokens)
|
6
|
-
super
|
7
|
-
|
45
|
+
def initialize(tag_name, markup, tokens)
|
8
46
|
if markup =~ Syntax
|
9
47
|
@variable_name = $1
|
10
48
|
@collection_name = $2
|
@@ -16,31 +54,30 @@ module Liquid
|
|
16
54
|
else
|
17
55
|
raise SyntaxError.new("Syntax Error in 'for loop' - Valid syntax: for [item] in [collection]")
|
18
56
|
end
|
57
|
+
|
58
|
+
super
|
19
59
|
end
|
20
60
|
|
21
61
|
def render(context)
|
22
62
|
context.registers[:for] ||= Hash.new(0)
|
23
63
|
|
24
64
|
collection = context[@collection_name]
|
65
|
+
collection = collection.to_a if collection.is_a?(Range)
|
25
66
|
|
26
67
|
return '' if collection.nil? or collection.empty?
|
27
68
|
|
28
69
|
range = (0..collection.length)
|
29
70
|
|
30
71
|
if @attributes['limit'] or @attributes['offset']
|
31
|
-
|
32
|
-
|
33
72
|
offset = 0
|
34
73
|
if @attributes['offset'] == 'continue'
|
35
74
|
offset = context.registers[:for][@name]
|
36
75
|
else
|
37
76
|
offset = context[@attributes['offset']] || 0
|
38
77
|
end
|
39
|
-
|
40
78
|
limit = context[@attributes['limit']]
|
41
79
|
|
42
80
|
range_end = limit ? offset + limit : collection.length
|
43
|
-
|
44
81
|
range = (offset..range_end-1)
|
45
82
|
|
46
83
|
# Save the range end in the registers so that future calls to
|
@@ -76,6 +113,6 @@ module Liquid
|
|
76
113
|
result
|
77
114
|
end
|
78
115
|
end
|
79
|
-
|
116
|
+
|
80
117
|
Template.register_tag('for', For)
|
81
118
|
end
|
data/lib/liquid/tags/if.rb
CHANGED
@@ -1,8 +1,22 @@
|
|
1
1
|
module Liquid
|
2
|
+
|
3
|
+
# If is the conditional block
|
4
|
+
#
|
5
|
+
# {% if user.admin %}
|
6
|
+
# Admin user!
|
7
|
+
# {% else %}
|
8
|
+
# Not admin user
|
9
|
+
# {% endif %}
|
10
|
+
#
|
11
|
+
# There are {% if count < 5 %} less {% else %} more {% endif %} items than you need.
|
12
|
+
#
|
13
|
+
#
|
2
14
|
class If < Block
|
3
|
-
|
15
|
+
SyntaxHelp = "Syntax Error in tag 'if' - Valid syntax: if [expression]"
|
16
|
+
Syntax = /(#{QuotedFragment})\s*([=!<>a-z_]+)?\s*(#{QuotedFragment})?/
|
17
|
+
|
18
|
+
def initialize(tag_name, markup, tokens)
|
4
19
|
|
5
|
-
def initialize(markup, tokens)
|
6
20
|
@blocks = []
|
7
21
|
|
8
22
|
push_block('if', markup)
|
@@ -32,19 +46,33 @@ module Liquid
|
|
32
46
|
private
|
33
47
|
|
34
48
|
def push_block(tag, markup)
|
35
|
-
|
36
49
|
block = if tag == 'else'
|
37
50
|
ElseCondition.new
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
raise
|
51
|
+
else
|
52
|
+
|
53
|
+
expressions = markup.split(/\b(and|or)\b/).reverse
|
54
|
+
raise SyntaxHelp unless expressions.shift =~ Syntax
|
55
|
+
|
56
|
+
condition = Condition.new($1, $2, $3)
|
57
|
+
|
58
|
+
while not expressions.empty?
|
59
|
+
operator = expressions.shift
|
60
|
+
|
61
|
+
raise SyntaxHelp unless expressions.shift.to_s =~ Syntax
|
62
|
+
|
63
|
+
new_condition = Condition.new($1, $2, $3)
|
64
|
+
new_condition.send(operator.to_sym, condition)
|
65
|
+
condition = new_condition
|
66
|
+
end
|
67
|
+
|
68
|
+
condition
|
42
69
|
end
|
43
70
|
|
44
71
|
@blocks.push(block)
|
45
72
|
@nodelist = block.attach(Array.new)
|
46
73
|
end
|
47
74
|
|
75
|
+
|
48
76
|
end
|
49
77
|
|
50
78
|
Template.register_tag('if', If)
|
data/lib/liquid/tags/include.rb
CHANGED
@@ -2,7 +2,7 @@ module Liquid
|
|
2
2
|
class Include < Tag
|
3
3
|
Syntax = /(#{QuotedFragment}+)(\s+(?:with|for)\s+(#{QuotedFragment}+))?/
|
4
4
|
|
5
|
-
def initialize(markup, tokens)
|
5
|
+
def initialize(tag_name, markup, tokens)
|
6
6
|
if markup =~ Syntax
|
7
7
|
|
8
8
|
@template_name = $1
|
@@ -27,8 +27,7 @@ module Liquid
|
|
27
27
|
source = Liquid::Template.file_system.read_template_file(context[@template_name])
|
28
28
|
partial = Liquid::Template.parse(source)
|
29
29
|
|
30
|
-
|
31
|
-
variable = context[@variable_name]
|
30
|
+
variable = context[@variable_name || @template_name[1..-2]]
|
32
31
|
|
33
32
|
context.stack do
|
34
33
|
@attributes.each do |key, value|
|
data/lib/liquid/tags/unless.rb
CHANGED
@@ -2,18 +2,22 @@ require File.dirname(__FILE__) + '/if'
|
|
2
2
|
|
3
3
|
module Liquid
|
4
4
|
|
5
|
+
# Unless is a conditional just like 'if' but works on the inverse logic.
|
6
|
+
#
|
7
|
+
# {% unless x < 0 %} x is greater than zero {% end %}
|
8
|
+
#
|
5
9
|
class Unless < If
|
6
10
|
def render(context)
|
7
11
|
context.stack do
|
8
12
|
|
9
13
|
# First condition is interpreted backwards ( if not )
|
10
|
-
block = @blocks.
|
14
|
+
block = @blocks.first
|
11
15
|
unless block.evaluate(context)
|
12
16
|
return render_all(block.attachment, context)
|
13
17
|
end
|
14
18
|
|
15
19
|
# After the first condition unless works just like if
|
16
|
-
@blocks.each do |block|
|
20
|
+
@blocks[1..-1].each do |block|
|
17
21
|
if block.evaluate(context)
|
18
22
|
return render_all(block.attachment, context)
|
19
23
|
end
|
data/lib/liquid/template.rb
CHANGED
@@ -70,19 +70,7 @@ module Liquid
|
|
70
70
|
def errors
|
71
71
|
@errors ||= []
|
72
72
|
end
|
73
|
-
|
74
|
-
def handle_error(e)
|
75
|
-
errors.push(e)
|
76
|
-
raise if @rethrow_errors
|
77
|
-
|
78
|
-
case e
|
79
|
-
when SyntaxError
|
80
|
-
"Liquid syntax error: #{e.message}"
|
81
|
-
else
|
82
|
-
"Liquid error: #{e.message}"
|
83
|
-
end
|
84
|
-
end
|
85
|
-
|
73
|
+
|
86
74
|
# Render takes a hash with local variables.
|
87
75
|
#
|
88
76
|
# if you use the same filters over and over again consider registering them globally
|
@@ -102,9 +90,11 @@ module Liquid
|
|
102
90
|
args.shift
|
103
91
|
when Hash
|
104
92
|
self.assigns.merge!(args.shift)
|
105
|
-
Context.new(
|
93
|
+
Context.new(assigns, registers, @rethrow_errors)
|
106
94
|
when nil
|
107
|
-
Context.new(
|
95
|
+
Context.new(assigns, registers, @rethrow_errors)
|
96
|
+
else
|
97
|
+
raise ArgumentError, "Expect Hash or Liquid::Context as parameter"
|
108
98
|
end
|
109
99
|
|
110
100
|
case args.last
|
@@ -123,10 +113,15 @@ module Liquid
|
|
123
113
|
when Array
|
124
114
|
context.add_filters(args.pop)
|
125
115
|
end
|
126
|
-
|
116
|
+
|
117
|
+
|
127
118
|
# render the nodelist.
|
128
119
|
# for performance reasons we get a array back here. to_s will make a string out of it
|
129
|
-
|
120
|
+
begin
|
121
|
+
@root.render(context).join
|
122
|
+
ensure
|
123
|
+
@errors = context.errors
|
124
|
+
end
|
130
125
|
end
|
131
126
|
|
132
127
|
def render!(*args)
|
@@ -144,7 +139,7 @@ module Liquid
|
|
144
139
|
tokens.shift if tokens[0] and tokens[0].empty?
|
145
140
|
|
146
141
|
tokens
|
147
|
-
end
|
142
|
+
end
|
148
143
|
|
149
144
|
end
|
150
145
|
end
|
data/lib/liquid/variable.rb
CHANGED
@@ -1,27 +1,40 @@
|
|
1
1
|
module Liquid
|
2
2
|
|
3
|
-
#
|
4
|
-
#
|
3
|
+
# Holds variables. Variables are only loaded "just in time"
|
4
|
+
# and are not evaluated as part of the render stage
|
5
|
+
#
|
6
|
+
# {{ monkey }}
|
7
|
+
# {{ user.name }}
|
8
|
+
#
|
9
|
+
# Variables can be combined with filters:
|
10
|
+
#
|
11
|
+
# {{ user | link }}
|
12
|
+
#
|
5
13
|
class Variable
|
6
14
|
attr_accessor :filters, :name
|
7
15
|
|
8
16
|
def initialize(markup)
|
9
|
-
@markup
|
10
|
-
@name
|
11
|
-
|
12
|
-
|
17
|
+
@markup = markup
|
18
|
+
@name = nil
|
19
|
+
@filters = []
|
20
|
+
if match = markup.match(/\s*(#{QuotedFragment})/)
|
21
|
+
@name = match[1]
|
22
|
+
if markup.match(/#{FilterSperator}\s*(.*)/)
|
23
|
+
filters = Regexp.last_match(1).split(/#{FilterSperator}/)
|
13
24
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
25
|
+
filters.each do |f|
|
26
|
+
if matches = f.match(/\s*(\w+)/)
|
27
|
+
filtername = matches[1]
|
28
|
+
filterargs = f.scan(/(?:#{FilterArgumentSeparator}|#{ArgumentSeparator})\s*(#{QuotedFragment})/).flatten
|
29
|
+
@filters << [filtername.to_sym, filterargs]
|
30
|
+
end
|
31
|
+
end
|
18
32
|
end
|
19
|
-
else
|
20
|
-
@filters = []
|
21
33
|
end
|
22
34
|
end
|
23
35
|
|
24
36
|
def render(context)
|
37
|
+
return '' if @name.nil?
|
25
38
|
output = context[@name]
|
26
39
|
@filters.inject(output) do |output, filter|
|
27
40
|
filterargs = filter[1].to_a.collect do |a|
|
data/test/block_test.rb
CHANGED
@@ -42,6 +42,14 @@ class VariableTest < Test::Unit::TestCase
|
|
42
42
|
assert_equal 3, template.root.nodelist.size
|
43
43
|
end
|
44
44
|
|
45
|
+
def test_with_custom_tag
|
46
|
+
Liquid::Template.register_tag("testtag", Block)
|
47
|
+
|
48
|
+
assert_nothing_thrown do
|
49
|
+
template = Liquid::Template.parse( "{% testtag %} {% endtesttag %}")
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
45
53
|
private
|
46
54
|
|
47
55
|
def block_types(nodelist)
|
@@ -0,0 +1,109 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/helper'
|
2
|
+
|
3
|
+
class ConditionTest < Test::Unit::TestCase
|
4
|
+
include Liquid
|
5
|
+
|
6
|
+
def test_basic_condition
|
7
|
+
assert_equal false, Condition.new('1', '==', '2').evaluate
|
8
|
+
assert_equal true, Condition.new('1', '==', '1').evaluate
|
9
|
+
end
|
10
|
+
|
11
|
+
def test_default_operators_evalute_true
|
12
|
+
assert_evalutes_true '1', '==', '1'
|
13
|
+
assert_evalutes_true '1', '!=', '2'
|
14
|
+
assert_evalutes_true '1', '<>', '2'
|
15
|
+
assert_evalutes_true '1', '<', '2'
|
16
|
+
assert_evalutes_true '2', '>', '1'
|
17
|
+
assert_evalutes_true '1', '>=', '1'
|
18
|
+
assert_evalutes_true '2', '>=', '1'
|
19
|
+
assert_evalutes_true '1', '<=', '2'
|
20
|
+
assert_evalutes_true '1', '<=', '1'
|
21
|
+
end
|
22
|
+
|
23
|
+
def test_default_operators_evalute_false
|
24
|
+
assert_evalutes_false '1', '==', '2'
|
25
|
+
assert_evalutes_false '1', '!=', '1'
|
26
|
+
assert_evalutes_false '1', '<>', '1'
|
27
|
+
assert_evalutes_false '1', '<', '0'
|
28
|
+
assert_evalutes_false '2', '>', '4'
|
29
|
+
assert_evalutes_false '1', '>=', '3'
|
30
|
+
assert_evalutes_false '2', '>=', '4'
|
31
|
+
assert_evalutes_false '1', '<=', '0'
|
32
|
+
assert_evalutes_false '1', '<=', '0'
|
33
|
+
end
|
34
|
+
|
35
|
+
def test_contains_works_on_strings
|
36
|
+
assert_evalutes_true "'bob'", 'contains', "'o'"
|
37
|
+
assert_evalutes_true "'bob'", 'contains', "'b'"
|
38
|
+
assert_evalutes_true "'bob'", 'contains', "'bo'"
|
39
|
+
assert_evalutes_true "'bob'", 'contains', "'ob'"
|
40
|
+
assert_evalutes_true "'bob'", 'contains', "'bob'"
|
41
|
+
|
42
|
+
assert_evalutes_false "'bob'", 'contains', "'bob2'"
|
43
|
+
assert_evalutes_false "'bob'", 'contains', "'a'"
|
44
|
+
assert_evalutes_false "'bob'", 'contains', "'---'"
|
45
|
+
end
|
46
|
+
|
47
|
+
def test_contains_works_on_arrays
|
48
|
+
@context = Liquid::Context.new
|
49
|
+
@context['array'] = [1,2,3,4,5]
|
50
|
+
|
51
|
+
assert_evalutes_false "array", 'contains', '0'
|
52
|
+
assert_evalutes_true "array", 'contains', '1'
|
53
|
+
assert_evalutes_true "array", 'contains', '2'
|
54
|
+
assert_evalutes_true "array", 'contains', '3'
|
55
|
+
assert_evalutes_true "array", 'contains', '4'
|
56
|
+
assert_evalutes_true "array", 'contains', '5'
|
57
|
+
assert_evalutes_false "array", 'contains', '6'
|
58
|
+
|
59
|
+
assert_evalutes_false "array", 'contains', '"1"'
|
60
|
+
|
61
|
+
end
|
62
|
+
|
63
|
+
def test_or_condition
|
64
|
+
condition = Condition.new('1', '==', '2')
|
65
|
+
|
66
|
+
assert_equal false, condition.evaluate
|
67
|
+
|
68
|
+
condition.or Condition.new('2', '==', '1')
|
69
|
+
|
70
|
+
assert_equal false, condition.evaluate
|
71
|
+
|
72
|
+
condition.or Condition.new('1', '==', '1')
|
73
|
+
|
74
|
+
assert_equal true, condition.evaluate
|
75
|
+
end
|
76
|
+
|
77
|
+
def test_and_condition
|
78
|
+
condition = Condition.new('1', '==', '1')
|
79
|
+
|
80
|
+
assert_equal true, condition.evaluate
|
81
|
+
|
82
|
+
condition.and Condition.new('2', '==', '2')
|
83
|
+
|
84
|
+
assert_equal true, condition.evaluate
|
85
|
+
|
86
|
+
condition.and Condition.new('2', '==', '1')
|
87
|
+
|
88
|
+
assert_equal false, condition.evaluate
|
89
|
+
end
|
90
|
+
|
91
|
+
|
92
|
+
def test_should_allow_custom_proc_operator
|
93
|
+
Condition.operators['starts_with'] = Proc.new { |cond, left, right| left =~ %r{^#{right}}}
|
94
|
+
|
95
|
+
assert_evalutes_true "'bob'", 'starts_with', "'b'"
|
96
|
+
assert_evalutes_false "'bob'", 'starts_with', "'o'"
|
97
|
+
ensure
|
98
|
+
Condition.operators.delete 'starts_with'
|
99
|
+
end
|
100
|
+
|
101
|
+
private
|
102
|
+
def assert_evalutes_true(left, op, right)
|
103
|
+
assert Condition.new(left, op, right).evaluate(@context || Liquid::Context.new), "Evaluated false: #{left} #{op} #{right}"
|
104
|
+
end
|
105
|
+
|
106
|
+
def assert_evalutes_false(left, op, right)
|
107
|
+
assert !Condition.new(left, op, right).evaluate(@context || Liquid::Context.new), "Evaluated true: #{left} #{op} #{right}"
|
108
|
+
end
|
109
|
+
end
|