liquid 2.2.2 → 2.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. data/{History.txt → History.md} +26 -23
  2. data/README.md +18 -16
  3. data/lib/liquid.rb +1 -4
  4. data/lib/liquid/block.rb +2 -2
  5. data/lib/liquid/context.rb +29 -33
  6. data/lib/liquid/drop.rb +11 -13
  7. data/lib/liquid/extensions.rb +13 -7
  8. data/lib/liquid/file_system.rb +2 -2
  9. data/lib/liquid/htmltags.rb +5 -4
  10. data/lib/liquid/standardfilters.rb +12 -1
  11. data/lib/liquid/strainer.rb +1 -1
  12. data/lib/liquid/tags/capture.rb +1 -1
  13. data/lib/liquid/tags/case.rb +4 -8
  14. data/lib/liquid/tags/decrement.rb +39 -0
  15. data/lib/liquid/tags/for.rb +20 -5
  16. data/lib/liquid/tags/include.rb +21 -12
  17. data/lib/liquid/tags/increment.rb +35 -0
  18. data/lib/liquid/tags/raw.rb +21 -0
  19. data/lib/liquid/tags/unless.rb +5 -5
  20. data/lib/liquid/template.rb +3 -2
  21. data/lib/liquid/variable.rb +1 -1
  22. data/test/liquid/assign_test.rb +15 -0
  23. data/test/liquid/block_test.rb +58 -0
  24. data/test/liquid/capture_test.rb +40 -0
  25. data/test/liquid/condition_test.rb +122 -0
  26. data/test/liquid/context_test.rb +478 -0
  27. data/test/liquid/drop_test.rb +162 -0
  28. data/test/liquid/error_handling_test.rb +81 -0
  29. data/test/liquid/file_system_test.rb +29 -0
  30. data/test/liquid/filter_test.rb +106 -0
  31. data/test/liquid/module_ex_test.rb +87 -0
  32. data/test/liquid/output_test.rb +116 -0
  33. data/test/liquid/parsing_quirks_test.rb +52 -0
  34. data/test/liquid/regexp_test.rb +44 -0
  35. data/test/liquid/security_test.rb +41 -0
  36. data/test/liquid/standard_filter_test.rb +191 -0
  37. data/test/liquid/strainer_test.rb +25 -0
  38. data/test/liquid/tags/html_tag_test.rb +29 -0
  39. data/test/liquid/tags/if_else_tag_test.rb +160 -0
  40. data/test/liquid/tags/include_tag_test.rb +139 -0
  41. data/test/liquid/tags/increment_tag_test.rb +24 -0
  42. data/test/liquid/tags/raw_tag_test.rb +15 -0
  43. data/test/liquid/tags/standard_tag_test.rb +461 -0
  44. data/test/liquid/tags/statements_test.rb +134 -0
  45. data/test/liquid/tags/unless_else_tag_test.rb +26 -0
  46. data/test/liquid/template_test.rb +74 -0
  47. data/test/liquid/variable_test.rb +170 -0
  48. data/test/test_helper.rb +29 -0
  49. metadata +67 -16
  50. data/Manifest.txt +0 -34
  51. data/lib/liquid/tags/literal.rb +0 -42
@@ -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
@@ -13,6 +13,8 @@ module Liquid
13
13
  # <div {% if forloop.first %}class="first"{% endif %}>
14
14
  # Item {{ forloop.index }}: {{ item.name }}
15
15
  # </div>
16
+ # {% else %}
17
+ # There is nothing in the collection.
16
18
  # {% endfor %}
17
19
  #
18
20
  # You can also define a limit and offset much like SQL. Remember
@@ -58,8 +60,14 @@ module Liquid
58
60
  raise SyntaxError.new("Syntax Error in 'for loop' - Valid syntax: for [item] in [collection]")
59
61
  end
60
62
 
63
+ @nodelist = @for_block = []
61
64
  super
62
65
  end
66
+
67
+ def unknown_tag(tag, markup, tokens)
68
+ return super unless tag == 'else'
69
+ @nodelist = @else_block = []
70
+ end
63
71
 
64
72
  def render(context)
65
73
  context.registers[:for] ||= Hash.new(0)
@@ -67,7 +75,7 @@ module Liquid
67
75
  collection = context[@collection_name]
68
76
  collection = collection.to_a if collection.is_a?(Range)
69
77
 
70
- return '' unless collection.respond_to?(:each)
78
+ return render_else(context) unless collection.respond_to?(:each)
71
79
 
72
80
  from = if @attributes['offset'] == 'continue'
73
81
  context.registers[:for][@name].to_i
@@ -81,11 +89,11 @@ module Liquid
81
89
 
82
90
  segment = slice_collection_using_each(collection, from, to)
83
91
 
84
- return '' if segment.empty?
92
+ return render_else(context) if segment.empty?
85
93
 
86
94
  segment.reverse! if @reversed
87
95
 
88
- result = []
96
+ result = ''
89
97
 
90
98
  length = segment.length
91
99
 
@@ -105,7 +113,7 @@ module Liquid
105
113
  'first' => (index == 0),
106
114
  'last' => (index == length - 1) }
107
115
 
108
- result << render_all(@nodelist, context)
116
+ result << render_all(@for_block, context)
109
117
  end
110
118
  end
111
119
  result
@@ -130,7 +138,14 @@ module Liquid
130
138
 
131
139
  segments
132
140
  end
141
+
142
+ private
143
+
144
+ def render_else(context)
145
+ return @else_block ? [render_all(@else_block, context)] : ''
146
+ end
147
+
133
148
  end
134
149
 
135
150
  Template.register_tag('for', For)
136
- end
151
+ end
@@ -20,14 +20,12 @@ module Liquid
20
20
  super
21
21
  end
22
22
 
23
- def parse(tokens)
23
+ def parse(tokens)
24
24
  end
25
-
26
- def render(context)
27
- file_system = context.registers[:file_system] || Liquid::Template.file_system
28
- source = file_system.read_template_file(context[@template_name])
29
- partial = Liquid::Template.parse(source)
30
-
25
+
26
+ def render(context)
27
+ source = _read_template_from_file_system(context)
28
+ partial = Liquid::Template.parse(source)
31
29
  variable = context[@variable_name || @template_name[1..-2]]
32
30
 
33
31
  context.stack do
@@ -36,20 +34,31 @@ module Liquid
36
34
  end
37
35
 
38
36
  if variable.is_a?(Array)
39
-
40
- variable.collect do |variable|
37
+ variable.collect do |variable|
41
38
  context[@template_name[1..-2]] = variable
42
39
  partial.render(context)
43
40
  end
44
-
45
41
  else
46
-
47
42
  context[@template_name[1..-2]] = variable
48
43
  partial.render(context)
49
-
50
44
  end
51
45
  end
52
46
  end
47
+
48
+ private
49
+ def _read_template_from_file_system(context)
50
+ file_system = context.registers[:file_system] || Liquid::Template.file_system
51
+
52
+ # make read_template_file call backwards-compatible.
53
+ case file_system.method(:read_template_file).arity
54
+ when 1
55
+ file_system.read_template_file(context[@template_name])
56
+ when 2
57
+ file_system.read_template_file(context[@template_name], context)
58
+ else
59
+ raise ArgumentError, "file_system.read_template_file expects two parameters: (template_name, context)"
60
+ end
61
+ end
53
62
  end
54
63
 
55
64
  Template.register_tag('include', Include)
@@ -0,0 +1,35 @@
1
+ module Liquid
2
+
3
+ # increment 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
+ # (To achieve the survival, the application must keep the context)
7
+ #
8
+ # if the variable does not exist, it is created with value 0.
9
+
10
+ # Hello: {% increment variable %}
11
+ #
12
+ # gives you:
13
+ #
14
+ # Hello: 0
15
+ # Hello: 1
16
+ # Hello: 2
17
+ #
18
+ class Increment < Tag
19
+ def initialize(tag_name, markup, tokens)
20
+ @variable = markup.strip
21
+
22
+ super
23
+ end
24
+
25
+ def render(context)
26
+ value = context.environments.first[@variable] ||= 0
27
+ context.environments.first[@variable] = value + 1
28
+ value.to_s
29
+ end
30
+
31
+ private
32
+ end
33
+
34
+ Template.register_tag('increment', Increment)
35
+ end
@@ -0,0 +1,21 @@
1
+ module Liquid
2
+ class Raw < Block
3
+ def parse(tokens)
4
+ @nodelist ||= []
5
+ @nodelist.clear
6
+
7
+ while token = tokens.shift
8
+ if token =~ FullToken
9
+ if block_delimiter == $1
10
+ end_tag
11
+ return
12
+ end
13
+ end
14
+ @nodelist << token if not token.empty?
15
+ end
16
+ end
17
+ end
18
+
19
+ Template.register_tag('raw', Raw)
20
+ end
21
+
@@ -13,19 +13,19 @@ module Liquid
13
13
  # First condition is interpreted backwards ( if not )
14
14
  block = @blocks.first
15
15
  unless block.evaluate(context)
16
- return render_all(block.attachment, context)
16
+ return render_all(block.attachment, context)
17
17
  end
18
18
 
19
19
  # After the first condition unless works just like if
20
20
  @blocks[1..-1].each do |block|
21
- if block.evaluate(context)
22
- return render_all(block.attachment, context)
21
+ if block.evaluate(context)
22
+ return render_all(block.attachment, context)
23
23
  end
24
- end
24
+ end
25
25
 
26
26
  ''
27
27
  end
28
- end
28
+ end
29
29
  end
30
30
 
31
31
 
@@ -55,7 +55,7 @@ module Liquid
55
55
  # Parse source code.
56
56
  # Returns self for easy chaining
57
57
  def parse(source)
58
- @root = Document.new(tokenize(Liquid::Literal.from_shorthand(source)))
58
+ @root = Document.new(tokenize(source))
59
59
  self
60
60
  end
61
61
 
@@ -121,7 +121,8 @@ module Liquid
121
121
  begin
122
122
  # render the nodelist.
123
123
  # for performance reasons we get a array back here. join will make a string out of it
124
- @root.render(context).join
124
+ result = @root.render(context)
125
+ result.respond_to?(:join) ? result.join : result
125
126
  ensure
126
127
  @errors = context.errors
127
128
  end
@@ -37,7 +37,7 @@ module Liquid
37
37
  return '' if @name.nil?
38
38
  @filters.inject(context[@name]) do |output, filter|
39
39
  filterargs = filter[1].to_a.collect do |a|
40
- context[a]
40
+ context[a]
41
41
  end
42
42
  begin
43
43
  output = context.invoke(filter[0], output, *filterargs)
@@ -0,0 +1,15 @@
1
+ require 'test_helper'
2
+
3
+ class AssignTest < Test::Unit::TestCase
4
+ include Liquid
5
+
6
+ def test_assigned_variable
7
+ assert_template_result('.foo.',
8
+ '{% assign foo = values %}.{{ foo[0] }}.',
9
+ 'values' => %w{foo bar baz})
10
+
11
+ assert_template_result('.bar.',
12
+ '{% assign foo = values %}.{{ foo[1] }}.',
13
+ 'values' => %w{foo bar baz})
14
+ end
15
+ end # AssignTest
@@ -0,0 +1,58 @@
1
+ require 'test_helper'
2
+
3
+ class VariableTest < Test::Unit::TestCase
4
+ include Liquid
5
+
6
+ def test_blankspace
7
+ template = Liquid::Template.parse(" ")
8
+ assert_equal [" "], template.root.nodelist
9
+ end
10
+
11
+ def test_variable_beginning
12
+ template = Liquid::Template.parse("{{funk}} ")
13
+ assert_equal 2, template.root.nodelist.size
14
+ assert_equal Variable, template.root.nodelist[0].class
15
+ assert_equal String, template.root.nodelist[1].class
16
+ end
17
+
18
+ def test_variable_end
19
+ template = Liquid::Template.parse(" {{funk}}")
20
+ assert_equal 2, template.root.nodelist.size
21
+ assert_equal String, template.root.nodelist[0].class
22
+ assert_equal Variable, template.root.nodelist[1].class
23
+ end
24
+
25
+ def test_variable_middle
26
+ template = Liquid::Template.parse(" {{funk}} ")
27
+ assert_equal 3, template.root.nodelist.size
28
+ assert_equal String, template.root.nodelist[0].class
29
+ assert_equal Variable, template.root.nodelist[1].class
30
+ assert_equal String, template.root.nodelist[2].class
31
+ end
32
+
33
+ def test_variable_many_embedded_fragments
34
+ template = Liquid::Template.parse(" {{funk}} {{so}} {{brother}} ")
35
+ assert_equal 7, template.root.nodelist.size
36
+ assert_equal [String, Variable, String, Variable, String, Variable, String],
37
+ block_types(template.root.nodelist)
38
+ end
39
+
40
+ def test_with_block
41
+ template = Liquid::Template.parse(" {% comment %} {% endcomment %} ")
42
+ assert_equal [String, Comment, String], block_types(template.root.nodelist)
43
+ assert_equal 3, template.root.nodelist.size
44
+ end
45
+
46
+ def test_with_custom_tag
47
+ Liquid::Template.register_tag("testtag", Block)
48
+
49
+ assert_nothing_thrown do
50
+ template = Liquid::Template.parse( "{% testtag %} {% endtesttag %}")
51
+ end
52
+ end
53
+
54
+ private
55
+ def block_types(nodelist)
56
+ nodelist.collect { |node| node.class }
57
+ end
58
+ end # VariableTest
@@ -0,0 +1,40 @@
1
+ require 'test_helper'
2
+
3
+ class CaptureTest < Test::Unit::TestCase
4
+ include Liquid
5
+
6
+ def test_captures_block_content_in_variable
7
+ assert_template_result("test string", "{% capture 'var' %}test string{% endcapture %}{{var}}", {})
8
+ end
9
+
10
+ def test_capture_to_variable_from_outer_scope_if_existing
11
+ template_source = <<-END_TEMPLATE
12
+ {% assign var = '' %}
13
+ {% if true %}
14
+ {% capture var %}first-block-string{% endcapture %}
15
+ {% endif %}
16
+ {% if true %}
17
+ {% capture var %}test-string{% endcapture %}
18
+ {% endif %}
19
+ {{var}}
20
+ END_TEMPLATE
21
+ template = Template.parse(template_source)
22
+ rendered = template.render
23
+ assert_equal "test-string", rendered.gsub(/\s/, '')
24
+ end
25
+
26
+ def test_assigning_from_capture
27
+ template_source = <<-END_TEMPLATE
28
+ {% assign first = '' %}
29
+ {% assign second = '' %}
30
+ {% for number in (1..3) %}
31
+ {% capture first %}{{number}}{% endcapture %}
32
+ {% assign second = first %}
33
+ {% endfor %}
34
+ {{ first }}-{{ second }}
35
+ END_TEMPLATE
36
+ template = Template.parse(template_source)
37
+ rendered = template.render
38
+ assert_equal "3-3", rendered.gsub(/\s/, '')
39
+ end
40
+ end # CaptureTest
@@ -0,0 +1,122 @@
1
+ require 'test_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
+ assert_evalutes_false "array", 'contains', '"1"'
59
+ end
60
+
61
+ def test_contains_returns_false_for_nil_operands
62
+ @context = Liquid::Context.new
63
+ assert_evalutes_false "not_assigned", 'contains', '0'
64
+ assert_evalutes_false "0", 'contains', 'not_assigned'
65
+ end
66
+
67
+ def test_or_condition
68
+ condition = Condition.new('1', '==', '2')
69
+
70
+ assert_equal false, condition.evaluate
71
+
72
+ condition.or Condition.new('2', '==', '1')
73
+
74
+ assert_equal false, condition.evaluate
75
+
76
+ condition.or Condition.new('1', '==', '1')
77
+
78
+ assert_equal true, condition.evaluate
79
+ end
80
+
81
+ def test_and_condition
82
+ condition = Condition.new('1', '==', '1')
83
+
84
+ assert_equal true, condition.evaluate
85
+
86
+ condition.and Condition.new('2', '==', '2')
87
+
88
+ assert_equal true, condition.evaluate
89
+
90
+ condition.and Condition.new('2', '==', '1')
91
+
92
+ assert_equal false, condition.evaluate
93
+ end
94
+
95
+ def test_should_allow_custom_proc_operator
96
+ Condition.operators['starts_with'] = Proc.new { |cond, left, right| left =~ %r{^#{right}} }
97
+
98
+ assert_evalutes_true "'bob'", 'starts_with', "'b'"
99
+ assert_evalutes_false "'bob'", 'starts_with', "'o'"
100
+
101
+ ensure
102
+ Condition.operators.delete 'starts_with'
103
+ end
104
+
105
+ def test_left_or_right_may_contain_operators
106
+ @context = Liquid::Context.new
107
+ @context['one'] = @context['another'] = "gnomeslab-and-or-liquid"
108
+
109
+ assert_evalutes_true "one", '==', "another"
110
+ end
111
+
112
+ private
113
+ def assert_evalutes_true(left, op, right)
114
+ assert Condition.new(left, op, right).evaluate(@context || Liquid::Context.new),
115
+ "Evaluated false: #{left} #{op} #{right}"
116
+ end
117
+
118
+ def assert_evalutes_false(left, op, right)
119
+ assert !Condition.new(left, op, right).evaluate(@context || Liquid::Context.new),
120
+ "Evaluated true: #{left} #{op} #{right}"
121
+ end
122
+ end # ConditionTest