| 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> |
| # {% else %} |
| # There is nothing in the collection. |
| # {% 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. |
| # forloop.parentloop:: Provides access to the parent loop, if present. |
| # |
| class For < Block |
| Syntax = /\A(#{VariableSegment}+)\s+in\s+(#{QuotedFragment}+)\s*(reversed)?/o |
| |
| def initialize(tag_name, markup, options) |
| super |
| @from = @limit = nil |
| parse_with_selected_parser(markup) |
| @for_block = BlockBody.new |
| @else_block = nil |
| end |
| |
| def parse(tokens) |
| return unless parse_body(@for_block, tokens) |
| parse_body(@else_block, tokens) |
| end |
| |
| def nodelist |
| @else_block ? [@for_block, @else_block] : [@for_block] |
| end |
| |
| def unknown_tag(tag, markup, tokens) |
| return super unless tag == 'else'.freeze |
| @else_block = BlockBody.new |
| end |
| |
| def render(context) |
| segment = collection_segment(context) |
| |
| if segment.empty? |
| render_else(context) |
| else |
| render_segment(context, segment) |
| end |
| end |
| |
| protected |
| |
| def lax_parse(markup) |
| if markup =~ Syntax |
| @variable_name = $1 |
| collection_name = $2 |
| @reversed = !!$3 |
| @name = "#{@variable_name}-#{collection_name}" |
| @collection_name = Expression.parse(collection_name) |
| markup.scan(TagAttributes) do |key, value| |
| set_attribute(key, value) |
| end |
| else |
| raise SyntaxError.new(options[:locale].t("errors.syntax.for".freeze)) |
| end |
| end |
| |
| def strict_parse(markup) |
| p = Parser.new(markup) |
| @variable_name = p.consume(:id) |
| raise SyntaxError.new(options[:locale].t("errors.syntax.for_invalid_in".freeze)) unless p.id?('in'.freeze) |
| collection_name = p.expression |
| @name = "#{@variable_name}-#{collection_name}" |
| @collection_name = Expression.parse(collection_name) |
| @reversed = p.id?('reversed'.freeze) |
| |
| while p.look(:id) && p.look(:colon, 1) |
| unless attribute = p.id?('limit'.freeze) || p.id?('offset'.freeze) |
| raise SyntaxError.new(options[:locale].t("errors.syntax.for_invalid_attribute".freeze)) |
| end |
| p.consume |
| set_attribute(attribute, p.expression) |
| end |
| p.consume(:end_of_string) |
| end |
| |
| private |
| |
| def collection_segment(context) |
| offsets = context.registers[:for] ||= Hash.new(0) |
| |
| from = if @from == :continue |
| offsets[@name].to_i |
| else |
| context.evaluate(@from).to_i |
| end |
| |
| collection = context.evaluate(@collection_name) |
| collection = collection.to_a if collection.is_a?(Range) |
| |
| limit = context.evaluate(@limit) |
| to = limit ? limit.to_i + from : nil |
| |
| segment = Utils.slice_collection(collection, from, to) |
| segment.reverse! if @reversed |
| |
| offsets[@name] = from + segment.length |
| |
| segment |
| end |
| |
| def render_segment(context, segment) |
| for_stack = context.registers[:for_stack] ||= [] |
| length = segment.length |
| |
| result = '' |
| |
| context.stack do |
| loop_vars = Liquid::ForloopDrop.new(@name, length, for_stack[-1]) |
| |
| for_stack.push(loop_vars) |
| |
| begin |
| context['forloop'.freeze] = loop_vars |
| |
| segment.each_with_index do |item, index| |
| context[@variable_name] = item |
| result << @for_block.render(context) |
| loop_vars.send(:increment!) |
| |
| # Handle any interrupts if they exist. |
| if context.interrupt? |
| interrupt = context.pop_interrupt |
| break if interrupt.is_a? BreakInterrupt |
| next if interrupt.is_a? ContinueInterrupt |
| end |
| end |
| ensure |
| for_stack.pop |
| end |
| end |
| |
| result |
| end |
| |
| def set_attribute(key, expr) |
| case key |
| when 'offset'.freeze |
| @from = if expr == 'continue'.freeze |
| :continue |
| else |
| Expression.parse(expr) |
| end |
| when 'limit'.freeze |
| @limit = Expression.parse(expr) |
| end |
| end |
| |
| def render_else(context) |
| @else_block ? @else_block.render(context) : ''.freeze |
| end |
| end |
| |
| Template.register_tag('for'.freeze, For) |
| end |