liquid 2.2.2 → 2.3.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.
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