public
Description: Liquid markup language. Save, customer facing template language for flexible web apps.
Homepage: http://www.liquidmarkup.org
Clone URL: git://github.com/tobi/liquid.git
Click here to lend your support to: liquid and make a donation at www.pledgie.com !
liquid / lib / liquid / context.rb
100644 246 lines (207 sloc) 6.499 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
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
module Liquid
 
  # Context keeps the variable stack and resolves variables, as well as keywords
  #
  # context['variable'] = 'testing'
  # context['variable'] #=> 'testing'
  # context['true'] #=> true
  # context['10.2232'] #=> 10.2232
  #
  # context.stack do
  # context['bob'] = 'bobsen'
  # end
  #
  # context['bob'] #=> nil class Context
  class Context
    attr_reader :scopes
    attr_reader :errors, :registers
 
    def initialize(assigns = {}, registers = {}, rethrow_errors = false)
      @scopes = [(assigns || {})]
      @registers = registers
      @errors = []
      @rethrow_errors = rethrow_errors
    end
 
    def strainer
      @strainer ||= Strainer.create(self)
    end
 
    # adds filters to this context.
    # this does not register the filters with the main Template object. see <tt>Template.register_filter</tt>
    # for that
    def add_filters(filters)
      filters = [filters].flatten.compact
 
      filters.each do |f|
        raise ArgumentError, "Expected module but got: #{f.class}" unless f.is_a?(Module)
        strainer.extend(f)
      end
    end
 
    def handle_error(e)
      errors.push(e)
      raise if @rethrow_errors
 
      case e
      when SyntaxError
        "Liquid syntax error: #{e.message}"
      else
        "Liquid error: #{e.message}"
      end
    end
 
 
    def invoke(method, *args)
      if strainer.respond_to?(method)
        strainer.__send__(method, *args)
      else
        args.first
      end
    end
 
    # push new local scope on the stack. use <tt>Context#stack</tt> instead
    def push
      raise StackLevelError, "Nesting too deep" if @scopes.length > 100
      @scopes.unshift({})
    end
 
    # merge a hash of variables in the current local scope
    def merge(new_scopes)
      @scopes[0].merge!(new_scopes)
    end
 
    # pop from the stack. use <tt>Context#stack</tt> instead
    def pop
      raise ContextError if @scopes.size == 1
      @scopes.shift
    end
 
    # pushes a new local scope on the stack, pops it at the end of the block
    #
    # Example:
    #
    # context.stack do
    # context['var'] = 'hi'
    # end
    # context['var] #=> nil
    #
    def stack(&block)
      result = nil
      push
      begin
        result = yield
      ensure
        pop
      end
      result
    end
 
    # Only allow String, Numeric, Hash, Array, Proc, Boolean or <tt>Liquid::Drop</tt>
    def []=(key, value)
      @scopes[0][key] = value
    end
 
    def [](key)
      resolve(key)
    end
 
    def has_key?(key)
      resolve(key) != nil
    end
 
    private
 
    # Look up variable, either resolve directly after considering the name. We can directly handle
    # Strings, digits, floats and booleans (true,false). If no match is made we lookup the variable in the current scope and
    # later move up to the parent blocks to see if we can resolve the variable somewhere up the tree.
    # Some special keywords return symbols. Those symbols are to be called on the rhs object in expressions
    #
    # Example:
    #
    # products == empty #=> products.empty?
    #
    def resolve(key)
      case key
      when nil, 'nil', 'null', ''
        nil
      when 'true'
        true
      when 'false'
        false
      when 'blank'
        :blank?
      when 'empty'
        :empty?
      # Single quoted strings
      when /^'(.*)'$/
        $1.to_s
      # Double quoted strings
      when /^"(.*)"$/
        $1.to_s
      # Integer and floats
      when /^(\d+)$/
        $1.to_i
      # Ranges
      when /^\((\S+)\.\.(\S+)\)$/
        (resolve($1).to_i..resolve($2).to_i)
      # Floats
      when /^(\d[\d\.]+)$/
        $1.to_f
      else
        variable(key)
      end
    end
 
    # fetches an object starting at the local scope and then moving up
    # the hierachy
    def find_variable(key)
      @scopes.each do |scope|
        if scope.has_key?(key)
          variable = scope[key]
          variable = scope[key] = variable.call(self) if variable.is_a?(Proc)
          variable = variable.to_liquid
          variable.context = self if variable.respond_to?(:context=)
          return variable
        end
      end
      nil
    end
 
    # resolves namespaced queries gracefully.
    #
    # Example
    #
    # @context['hash'] = {"name" => 'tobi'}
    # assert_equal 'tobi', @context['hash.name']
    # assert_equal 'tobi', @context['hash[name]']
    #
    def variable(markup)
      parts = markup.scan(VariableParser)
      square_bracketed = /^\[(.*)\]$/
 
      first_part = parts.shift
      if first_part =~ square_bracketed
        first_part = resolve($1)
      end
 
      if object = find_variable(first_part)
 
        parts.each do |part|
 
          # If object is a hash we look for the presence of the key and if its available
          # we return it
 
          if part =~ square_bracketed
            part = resolve($1)
 
            object[pos] = object[part].call(self) if object[part].is_a?(Proc) and object.respond_to?(:[]=)
            object = object[part].to_liquid
 
          else
 
            # Hash
            if object.respond_to?(:has_key?) and object.has_key?(part)
 
              # if its a proc we will replace the entry in the hash table with the proc
              res = object[part]
              res = object[part] = res.call(self) if res.is_a?(Proc) and object.respond_to?(:[]=)
              object = res.to_liquid
 
            # Array
            elsif object.respond_to?(:fetch) and part =~ /^\d+$/
              pos = part.to_i
 
              object[pos] = object[pos].call(self) if object[pos].is_a?(Proc) and object.respond_to?(:[]=)
              object = object[pos].to_liquid
 
            # Some special cases. If no key with the same name was found we interpret following calls
            # as commands and call them on the current object
            elsif object.respond_to?(part) and ['size', 'first', 'last'].include?(part)
 
              object = object.send(part.intern).to_liquid
 
            # No key was present with the desired value and it wasn't one of the directly supported
            # keywords either. The only thing we got left is to return nil
            else
              return nil
            end
          end
 
          # If we are dealing with a drop here we have to
          object.context = self if object.respond_to?(:context=)
        end
      end
 
      object
    end
 
    private
 
    def execute_proc(proc)
      proc.call(self)
    end
  end
end