tobi / liquid

Liquid markup language. Save, customer facing template language for flexible web apps.

This URL has Read+Write access

liquid / lib / liquid / tags / for.rb
100644 136 lines (113 sloc) 4.294 kb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
module Liquid
 
  # "For" iterates over an array or collection.
  # Several useful variables are available to you within the loop.
  #
  # == Basic usage:
  # {% for item in collection %}
  # {{ forloop.index }}: {{ item.name }}
  # {% endfor %}
  #
  # == Advanced usage:
  # {% for item in collection %}
  # <div {% if forloop.first %}class="first"{% endif %}>
  # Item {{ forloop.index }}: {{ item.name }}
  # </div>
  # {% endfor %}
  #
  # You can also define a limit and offset much like SQL. Remember
  # that offset starts at 0 for the first item.
  #
  # {% for item in collection limit:5 offset:10 %}
  # {{ item.name }}
  # {% end %}
  #
  # To reverse the for loop simply use {% for item in collection reversed %}
  #
  # == Available variables:
  #
  # forloop.name:: 'item-collection'
  # forloop.length:: Length of the loop
  # forloop.index:: The current item's position in the collection;
  # forloop.index starts at 1.
  # This is helpful for non-programmers who start believe
  # the first item in an array is 1, not 0.
  # forloop.index0:: The current item's position in the collection
  # where the first item is 0
  # forloop.rindex:: Number of items remaining in the loop
  # (length - index) where 1 is the last item.
  # forloop.rindex0:: Number of items remaining in the loop
  # where 0 is the last item.
  # forloop.first:: Returns true if the item is the first item.
  # forloop.last:: Returns true if the item is the last item.
  #
  class For < Block
    Syntax = /(\w+)\s+in\s+(#{QuotedFragment}+)\s*(reversed)?/
  
    def initialize(tag_name, markup, tokens)
      if markup =~ Syntax
        @variable_name = $1
        @collection_name = $2
        @name = "#{$1}-#{$2}"
        @reversed = $3
        @attributes = {}
        markup.scan(TagAttributes) do |key, value|
          @attributes[key] = value
        end
      else
        raise SyntaxError.new("Syntax Error in 'for loop' - Valid syntax: for [item] in [collection]")
      end
 
      super
    end
  
    def render(context)
      context.registers[:for] ||= Hash.new(0)
    
      collection = context[@collection_name]
      collection = collection.to_a if collection.is_a?(Range)
    
      return '' unless collection.respond_to?(:each)
                                                 
      from = if @attributes['offset'] == 'continue'
        context.registers[:for][@name].to_i
      else
        context[@attributes['offset']].to_i
      end
        
      limit = context[@attributes['limit']]
      to = limit ? limit.to_i + from : nil
          
                       
      segment = slice_collection_using_each(collection, from, to)
      
      return '' if segment.empty?
      
      segment.reverse! if @reversed
 
      result = []
        
      length = segment.length
            
      # Store our progress through the collection for the continue flag
      context.registers[:for][@name] = from + segment.length
              
      context.stack do
        segment.each_with_index do |item, index|
          context[@variable_name] = item
          context['forloop'] = {
            'name' => @name,
            'length' => length,
            'index' => index + 1,
            'index0' => index,
            'rindex' => length - index,
            'rindex0' => length - index -1,
            'first' => (index == 0),
            'last' => (index == length - 1) }
 
          result << render_all(@nodelist, context)
        end
      end
      result
    end
        
    def slice_collection_using_each(collection, from, to)
      segments = []
      index = 0
      yielded = 0
      collection.each do |item|
                
        if to && to <= index
          break
        end
        
        if from <= index
          segments << item
        end
                
        index += 1
      end
 
      segments
    end
  end
 
  Template.register_tag('for', For)
end