liquid 1.7.0 → 1.9.0

Sign up to get free protection for your applications and to get access to all the features.
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