~ubuntu-branches/ubuntu/trusty/ruby-liquid/trusty-proposed

« back to all changes in this revision

Viewing changes to lib/liquid/context.rb

  • Committer: Package Import Robot
  • Author(s): Cédric Boutillier
  • Date: 2011-10-22 00:11:06 UTC
  • Revision ID: package-import@ubuntu.com-20111022001106-ajpsf9ov2st9oxdx
Tags: upstream-2.3.0
Import upstream version 2.3.0

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
module Liquid
 
2
 
 
3
  # Context keeps the variable stack and resolves variables, as well as keywords
 
4
  #
 
5
  #   context['variable'] = 'testing'
 
6
  #   context['variable'] #=> 'testing'
 
7
  #   context['true']     #=> true
 
8
  #   context['10.2232']  #=> 10.2232
 
9
  #
 
10
  #   context.stack do
 
11
  #      context['bob'] = 'bobsen'
 
12
  #   end
 
13
  #
 
14
  #   context['bob']  #=> nil  class Context
 
15
  class Context
 
16
    attr_reader :scopes, :errors, :registers, :environments
 
17
 
 
18
    def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false)
 
19
      @environments   = [environments].flatten
 
20
      @scopes         = [(outer_scope || {})]
 
21
      @registers      = registers
 
22
      @errors         = []
 
23
      @rethrow_errors = rethrow_errors
 
24
      squash_instance_assigns_with_environments
 
25
    end
 
26
 
 
27
    def strainer
 
28
      @strainer ||= Strainer.create(self)
 
29
    end
 
30
 
 
31
    # Adds filters to this context.
 
32
    #
 
33
    # Note that this does not register the filters with the main Template object. see <tt>Template.register_filter</tt>
 
34
    # for that
 
35
    def add_filters(filters)
 
36
      filters = [filters].flatten.compact
 
37
 
 
38
      filters.each do |f|
 
39
        raise ArgumentError, "Expected module but got: #{f.class}" unless f.is_a?(Module)
 
40
        strainer.extend(f)
 
41
      end
 
42
    end
 
43
 
 
44
    def handle_error(e)
 
45
      errors.push(e)
 
46
      raise if @rethrow_errors
 
47
 
 
48
      case e
 
49
      when SyntaxError
 
50
        "Liquid syntax error: #{e.message}"
 
51
      else
 
52
        "Liquid error: #{e.message}"
 
53
      end
 
54
    end
 
55
 
 
56
    def invoke(method, *args)
 
57
      if strainer.respond_to?(method)
 
58
        strainer.__send__(method, *args)
 
59
      else
 
60
        args.first
 
61
      end
 
62
    end
 
63
 
 
64
    # Push new local scope on the stack. use <tt>Context#stack</tt> instead
 
65
    def push(new_scope={})
 
66
      @scopes.unshift(new_scope)
 
67
      raise StackLevelError, "Nesting too deep" if @scopes.length > 100
 
68
    end
 
69
 
 
70
    # Merge a hash of variables in the current local scope
 
71
    def merge(new_scopes)
 
72
      @scopes[0].merge!(new_scopes)
 
73
    end
 
74
 
 
75
    # Pop from the stack. use <tt>Context#stack</tt> instead
 
76
    def pop
 
77
      raise ContextError if @scopes.size == 1
 
78
      @scopes.shift
 
79
    end
 
80
 
 
81
    # Pushes a new local scope on the stack, pops it at the end of the block
 
82
    #
 
83
    # Example:
 
84
    #   context.stack do
 
85
    #      context['var'] = 'hi'
 
86
    #   end
 
87
    #
 
88
    #   context['var]  #=> nil
 
89
    def stack(new_scope={})
 
90
      push(new_scope)
 
91
      yield
 
92
    ensure
 
93
      pop
 
94
    end
 
95
 
 
96
    def clear_instance_assigns
 
97
      @scopes[0] = {}
 
98
    end
 
99
 
 
100
    # Only allow String, Numeric, Hash, Array, Proc, Boolean or <tt>Liquid::Drop</tt>
 
101
    def []=(key, value)
 
102
      @scopes[0][key] = value
 
103
    end
 
104
 
 
105
    def [](key)
 
106
      resolve(key)
 
107
    end
 
108
 
 
109
    def has_key?(key)
 
110
      resolve(key) != nil
 
111
    end
 
112
 
 
113
    private
 
114
      LITERALS = {
 
115
        nil => nil, 'nil' => nil, 'null' => nil, '' => nil,
 
116
        'true'  => true,
 
117
        'false' => false,
 
118
        'blank' => :blank?,
 
119
        'empty' => :empty?
 
120
      }
 
121
 
 
122
      # Look up variable, either resolve directly after considering the name. We can directly handle
 
123
      # Strings, digits, floats and booleans (true,false).
 
124
      # If no match is made we lookup the variable in the current scope and
 
125
      # later move up to the parent blocks to see if we can resolve the variable somewhere up the tree.
 
126
      # Some special keywords return symbols. Those symbols are to be called on the rhs object in expressions
 
127
      #
 
128
      # Example:
 
129
      #   products == empty #=> products.empty?
 
130
      def resolve(key)
 
131
        if LITERALS.key?(key)
 
132
          LITERALS[key]
 
133
        else
 
134
          case key
 
135
          when /^'(.*)'$/ # Single quoted strings
 
136
            $1
 
137
          when /^"(.*)"$/ # Double quoted strings
 
138
            $1
 
139
          when /^(\d+)$/ # Integer and floats
 
140
            $1.to_i
 
141
          when /^\((\S+)\.\.(\S+)\)$/ # Ranges
 
142
            (resolve($1).to_i..resolve($2).to_i)
 
143
          when /^(\d[\d\.]+)$/ # Floats
 
144
            $1.to_f
 
145
          else
 
146
            variable(key)
 
147
          end
 
148
        end
 
149
      end
 
150
 
 
151
      # Fetches an object starting at the local scope and then moving up the hierachy
 
152
      def find_variable(key)
 
153
        scope = @scopes.find { |s| s.has_key?(key) }
 
154
 
 
155
        if scope.nil?
 
156
          @environments.each do |e|
 
157
            if variable = lookup_and_evaluate(e, key)
 
158
              scope = e
 
159
              break
 
160
            end
 
161
          end
 
162
        end
 
163
 
 
164
        scope     ||= @environments.last || @scopes.last
 
165
        variable  ||= lookup_and_evaluate(scope, key)
 
166
 
 
167
        variable = variable.to_liquid
 
168
        variable.context = self if variable.respond_to?(:context=)
 
169
 
 
170
        return variable
 
171
      end
 
172
 
 
173
      # Resolves namespaced queries gracefully.
 
174
      #
 
175
      # Example
 
176
      #  @context['hash'] = {"name" => 'tobi'}
 
177
      #  assert_equal 'tobi', @context['hash.name']
 
178
      #  assert_equal 'tobi', @context['hash["name"]']
 
179
      def variable(markup)
 
180
        parts = markup.scan(VariableParser)
 
181
        square_bracketed = /^\[(.*)\]$/
 
182
 
 
183
        first_part = parts.shift
 
184
 
 
185
        if first_part =~ square_bracketed
 
186
          first_part = resolve($1)
 
187
        end
 
188
 
 
189
        if object = find_variable(first_part)
 
190
 
 
191
          parts.each do |part|
 
192
            part = resolve($1) if part_resolved = (part =~ square_bracketed)
 
193
 
 
194
            # If object is a hash- or array-like object we look for the
 
195
            # presence of the key and if its available we return it
 
196
            if object.respond_to?(:[]) and
 
197
              ((object.respond_to?(:has_key?) and object.has_key?(part)) or
 
198
               (object.respond_to?(:fetch) and part.is_a?(Integer)))
 
199
 
 
200
              # if its a proc we will replace the entry with the proc
 
201
              res = lookup_and_evaluate(object, part)
 
202
              object = res.to_liquid
 
203
 
 
204
              # Some special cases. If the part wasn't in square brackets and
 
205
              # no key with the same name was found we interpret following calls
 
206
              # as commands and call them on the current object
 
207
            elsif !part_resolved and object.respond_to?(part) and ['size', 'first', 'last'].include?(part)
 
208
 
 
209
              object = object.send(part.intern).to_liquid
 
210
 
 
211
              # No key was present with the desired value and it wasn't one of the directly supported
 
212
              # keywords either. The only thing we got left is to return nil
 
213
            else
 
214
              return nil
 
215
            end
 
216
 
 
217
            # If we are dealing with a drop here we have to
 
218
            object.context = self if object.respond_to?(:context=)
 
219
          end
 
220
        end
 
221
 
 
222
        object
 
223
      end # variable
 
224
 
 
225
      def lookup_and_evaluate(obj, key)
 
226
        if (value = obj[key]).is_a?(Proc) && obj.respond_to?(:[]=)
 
227
          obj[key] = (value.arity == 0) ? value.call : value.call(self)
 
228
        else
 
229
          value
 
230
        end
 
231
      end # lookup_and_evaluate
 
232
 
 
233
      def squash_instance_assigns_with_environments
 
234
        @scopes.last.each_key do |k|
 
235
          @environments.each do |env|
 
236
            if env.has_key?(k)
 
237
              scopes.last[k] = lookup_and_evaluate(env, k)
 
238
              break
 
239
            end
 
240
          end
 
241
        end
 
242
      end # squash_instance_assigns_with_environments
 
243
  end # Context
 
244
 
 
245
end # Liquid