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.
Files changed (46) hide show
  1. data/CHANGELOG +17 -15
  2. data/History.txt +44 -0
  3. data/MIT-LICENSE +2 -2
  4. data/Manifest.txt +6 -1
  5. data/{README → README.txt} +0 -0
  6. data/Rakefile +3 -3
  7. data/init.rb +5 -3
  8. data/lib/liquid.rb +8 -6
  9. data/lib/liquid/block.rb +6 -9
  10. data/lib/liquid/condition.rb +49 -17
  11. data/lib/liquid/context.rb +67 -41
  12. data/lib/liquid/errors.rb +8 -5
  13. data/lib/liquid/htmltags.rb +17 -7
  14. data/lib/liquid/module_ex.rb +62 -0
  15. data/lib/liquid/standardfilters.rb +39 -0
  16. data/lib/liquid/strainer.rb +20 -11
  17. data/lib/liquid/tag.rb +4 -3
  18. data/lib/liquid/tags/assign.rb +15 -4
  19. data/lib/liquid/tags/capture.rb +15 -2
  20. data/lib/liquid/tags/case.rb +51 -36
  21. data/lib/liquid/tags/cycle.rb +16 -2
  22. data/lib/liquid/tags/for.rb +45 -8
  23. data/lib/liquid/tags/if.rb +35 -7
  24. data/lib/liquid/tags/include.rb +2 -3
  25. data/lib/liquid/tags/unless.rb +6 -2
  26. data/lib/liquid/template.rb +13 -18
  27. data/lib/liquid/variable.rb +25 -12
  28. data/test/block_test.rb +8 -0
  29. data/test/condition_test.rb +109 -0
  30. data/test/context_test.rb +88 -10
  31. data/test/drop_test.rb +3 -1
  32. data/test/error_handling_test.rb +16 -3
  33. data/test/extra/breakpoint.rb +0 -0
  34. data/test/extra/caller.rb +0 -0
  35. data/test/filter_test.rb +3 -3
  36. data/test/html_tag_test.rb +7 -0
  37. data/test/if_else_test.rb +32 -0
  38. data/test/include_tag_test.rb +24 -1
  39. data/test/module_ex_test.rb +89 -0
  40. data/test/parsing_quirks_test.rb +15 -0
  41. data/test/regexp_test.rb +4 -3
  42. data/test/standard_filter_test.rb +27 -2
  43. data/test/standard_tag_test.rb +67 -20
  44. data/test/test_helper.rb +20 -0
  45. data/test/unless_else_test.rb +8 -0
  46. metadata +60 -46
@@ -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)
@@ -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
@@ -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
- Syntax = /(#{QuotedFragment})\s*([=!<>]+)?\s*(#{QuotedFragment})?/
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
- elsif markup =~ Syntax
39
- Condition.new($1, $2, $3)
40
- else
41
- raise SyntaxError.new("Syntax Error in tag '#{tag}' - Valid syntax: #{tag} [condition]")
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)
@@ -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|
@@ -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.shift
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
@@ -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(self)
93
+ Context.new(assigns, registers, @rethrow_errors)
106
94
  when nil
107
- Context.new(self)
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
- @root.render(context).to_s
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
@@ -1,27 +1,40 @@
1
1
  module Liquid
2
2
 
3
- # Hols variables. Variables are only loaded "just in time"
4
- # they are not evaluated as part of the render stage
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 = markup
10
- @name = markup.match(/\s*(#{QuotedFragment})/)[1]
11
- if markup.match(/#{FilterSperator}\s*(.*)/)
12
- filters = Regexp.last_match(1).split(/#{FilterSperator}/)
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
- @filters = filters.collect do |f|
15
- filtername = f.match(/\s*(\w+)/)[1]
16
- filterargs = f.scan(/(?:#{FilterArgumentSeparator}|#{ArgumentSeparator})\s*(#{QuotedFragment})/).flatten
17
- [filtername.to_sym, filterargs]
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