Class: Liquid2::RenderContext

Inherits:
Object
  • Object
show all
Defined in:
lib/liquid2/context.rb

Overview

Per render contextual information. A new RenderContext is created automatically every time Template#render is called.

Constant Summary collapse

BUILT_IN =
BuiltIn.new

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(template, globals: nil, disabled_tags: nil, copy_depth: 0, parent: nil, loop_carry: 1, local_namespace_carry: 0) ⇒ RenderContext

Returns a new instance of RenderContext.

Parameters:

  • template (Template)
  • globals (Hash<String, Object>?) (defaults to: nil)
  • disabled_tags (Array<String>?) (defaults to: nil)
  • copy_depth (Integer?) (defaults to: 0)
  • parent (RenderContext?) (defaults to: nil)
  • parent_scope (Array[_Namespace])

    Namespaces from a parent render context.

  • loop_carry (Integer?) (defaults to: 1)
  • local_namespace_carry (Integer?) (defaults to: 0)


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
# File 'lib/liquid2/context.rb', line 45

def initialize(
  template,
  globals: nil,
  disabled_tags: nil,
  copy_depth: 0,
  parent: nil,
  loop_carry: 1,
  local_namespace_carry: 0
)
  @env = template.env
  @template = template
  @globals = globals || {} # steep:ignore UnannotatedEmptyCollection
  @disabled_tags = disabled_tags || []
  @copy_depth = copy_depth
  @parent = parent
  @loop_carry = loop_carry

  # The current size of the local namespace. _size_ is a non-specific measure of the
  # amount of memory used to store template local variables.
  @assign_score = local_namespace_carry

  # A namespace for template local variables (those bound with `assign` or `capture`).
  @locals = {}

  # A namespace for `increment` and `decrement` counters.
  @counters = Hash.new(0)

  # Namespaces are searched from right to left. When a RenderContext is extended, the
  # temporary namespace is pushed to the end of this queue.
  @scope = ReadOnlyChainHash.new(@counters, BUILT_IN, @globals, @locals)

  # A namespace supporting stateful tags, such as `cycle` and `increment`.
  # It's OK to use this hash for storing custom tag state.
  @tag_namespace = {
    cycles: Hash.new(0),
    stop_index: {},
    extends: Hash.new { |hash, key| hash[key] = [] },
    macros: {}
  }

  # A stack of forloop objs used for populating forloop.parentloop.
  @loops = [] # : Array[ForLoop]

  # A stack of interrupts used to signal breaking and continuing `for` loops.
  @interrupts = [] # : Array[Symbol]
end

Instance Attribute Details

#disabled_tagsObject (readonly)

Returns the value of attribute disabled_tags.



32
33
34
# File 'lib/liquid2/context.rb', line 32

def disabled_tags
  @disabled_tags
end

#envObject (readonly)

Returns the value of attribute env.



32
33
34
# File 'lib/liquid2/context.rb', line 32

def env
  @env
end

#globalsObject (readonly)

Returns the value of attribute globals.



32
33
34
# File 'lib/liquid2/context.rb', line 32

def globals
  @globals
end

#interruptsObject

Returns the value of attribute interrupts.



33
34
35
# File 'lib/liquid2/context.rb', line 33

def interrupts
  @interrupts
end

#tag_namespaceObject

Returns the value of attribute tag_namespace.



33
34
35
# File 'lib/liquid2/context.rb', line 33

def tag_namespace
  @tag_namespace
end

#templateObject

Returns the value of attribute template.



33
34
35
# File 'lib/liquid2/context.rb', line 33

def template
  @template
end

Instance Method Details

#assign(key, value) ⇒ nil Also known as: []=

Add key to the local scope with value value.

Parameters:

  • key (String)
  • value (Object)

Returns:

  • (nil)


101
102
103
104
105
106
107
108
109
# File 'lib/liquid2/context.rb', line 101

def assign(key, value)
  @locals[key] = value
  if (limit = @env.local_namespace_limit)
    # Note that this approach does not account for overwriting keys/values
    # in the local scope. The assign score is always incremented.
    @assign_score += assign_score(value)
    raise LiquidResourceLimitError, "local namespace limit reached" if @assign_score > limit
  end
end

#copy(namespace, template: nil, disabled_tags: nil, carry_loop_iterations: false, block_scope: false) ⇒ Object

Copy this render context and add namespace to the new scope.

Parameters:

  • namespace (Hash<String, Object>)
  • template (Template?) (defaults to: nil)

    The template obj bound to the new context.

  • disabled_tags (Set<String>) (defaults to: nil)

    Names of tags to disallow in the new context.

  • carry_loop_iterations (bool) (defaults to: false)

    If true, pass the current loop iteration count to the new context.

  • block_scope (bool) (defaults to: false)

    It true, retain the current scope in the new context. Otherwise only global variables will be included in the new context's scope.



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
# File 'lib/liquid2/context.rb', line 193

def copy(namespace,
         template: nil,
         disabled_tags: nil,
         carry_loop_iterations: false,
         block_scope: false)
  if @copy_depth > @env.context_depth_limit
    raise LiquidResourceLimitError, "context depth limit reached"
  end

  loop_carry = if carry_loop_iterations
                 @loops.map(&:length).reduce(@loop_carry) { |acc, value| acc * value }
               else
                 1
               end

  scope = if block_scope
            ReadOnlyChainHash.new(@scope, namespace)
          else
            ReadOnlyChainHash.new(@globals, namespace)
          end

  context = self.class.new(template || @template,
                           globals: scope,
                           disabled_tags: disabled_tags,
                           copy_depth: @copy_depth + 1,
                           parent: self,
                           loop_carry: loop_carry,
                           local_namespace_carry: @assign_score)

  @env.persistent_namespaces.each do |ns|
    context.tag_namespace[ns] = @tag_namespace[ns] if @tag_namespace[ns]
  end

  context
end

#decrement(name) ⇒ Object



289
290
291
292
293
# File 'lib/liquid2/context.rb', line 289

def decrement(name)
  val = @counters[name] - 1
  @counters[name] = val
  val
end

#evaluate(obj) ⇒ Object

Evaluate obj as an expression in the render current context.



93
94
95
# File 'lib/liquid2/context.rb', line 93

def evaluate(obj)
  obj.respond_to?(:evaluate) ? obj.evaluate(self) : obj
end

#extend(namespace, template: nil) ⇒ Object

Extend the scope of this context with the given namespace. Expects a block.

Parameters:

  • namespace (Hash<String, Object>)
  • template (Template?) (defaults to: nil)

    Replace the current template for the duration of the block.



165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
# File 'lib/liquid2/context.rb', line 165

def extend(namespace, template: nil)
  if @scope.size > @env.context_depth_limit
    raise LiquidResourceLimitError, "context depth limit reached"
  end

  template_ = @template
  @template = template if template
  @scope << namespace
  begin
    yield
  rescue LiquidError => e
    e.template_name = template.full_name if template && !e.template_name
    e.source = template.source if template && !e.source
    raise
  ensure
    @template = template_
    @scope.pop
  end
end

#fetch(head, path, node:, default: :undefined) ⇒ Object

Resolve path to variable/data in the current scope.

Parameters:

  • head (String|Integer)

    First segment of the path.

  • path (Array<String|Integer>)

    Remaining path segments.

  • node (Node?)

    An associated token to use for error context.

  • default (Object?) (defaults to: :undefined)

    A default value to return if the path can no be resolved.

Returns:

  • (Object)


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
# File 'lib/liquid2/context.rb', line 119

def fetch(head, path, node:, default: :undefined)
  obj = @scope.fetch(evaluate(head))

  if obj == :undefined
    return @env.undefined(head, node: node) if default == :undefined

    return default
  end

  index = 0
  while (segment = path[index])
    index += 1
    segment = evaluate(segment)
    segment = segment.to_liquid(self) if segment.respond_to?(:to_liquid)

    if obj.respond_to?(:[]) &&
       ((obj.respond_to?(:key?) && obj.key?(segment)) ||
        (obj.respond_to?(:fetch) && segment.is_a?(Integer)))
      obj = obj[segment]
      next
    end

    obj = if segment == "size" && obj.respond_to?(:size)
            obj.size
          elsif segment == "first" && obj.respond_to?(:first)
            obj.first
          elsif segment == "last" && obj.respond_to?(:last)
            obj.last
          else
            return default == :undefined ? @env.undefined(head, node: node) : default
          end
  end

  obj
end

#get_output_buffer(parent_buffer) ⇒ Object



276
277
278
279
280
281
# File 'lib/liquid2/context.rb', line 276

def get_output_buffer(parent_buffer)
  return StringIO.new unless @env.output_stream_limit

  carry = parent_buffer.is_a?(LimitedStringIO) ? parent_buffer.size : 0
  LimitedStringIO.new((@env.output_stream_limit || raise) - carry)
end

#increment(name) ⇒ Object



283
284
285
286
287
# File 'lib/liquid2/context.rb', line 283

def increment(name)
  val = @counters[name]
  @counters[name] = val + 1
  val
end

#loop(namespace, forloop) ⇒ Object

Push a new namespace and forloop for the duration of a block.

Parameters:

  • namespace (Hash<String, Object>)
  • forloop (ForLoop)


232
233
234
235
236
237
238
239
240
241
242
# File 'lib/liquid2/context.rb', line 232

def loop(namespace, forloop)
  raise_for_loop_limit(length: forloop.length)
  @loops << forloop
  @scope << namespace
  begin
    yield
  ensure
    @scope.pop
    @loops.pop
  end
end

#parent_loop(node) ⇒ Object

Return the last ForLoop obj if one is available, or an instance of Undefined otherwise.



245
246
247
248
249
# File 'lib/liquid2/context.rb', line 245

def parent_loop(node)
  return @env.undefined("parentloop", node: node) if @loops.empty?

  @loops.last
end

#raise_for_loop_limit(length: 1) ⇒ Object



260
261
262
263
264
265
266
267
268
# File 'lib/liquid2/context.rb', line 260

def raise_for_loop_limit(length: 1)
  return nil unless @env.loop_iteration_limit

  loop_count = @loops.map(&:length).reduce(length * @loop_carry) { |acc, value| acc * value }

  return unless loop_count > (@env.loop_iteration_limit || raise)

  raise LiquidResourceLimitError, "loop iteration limit reached"
end

#raise_for_output_limit(length) ⇒ Object



270
271
272
273
274
# File 'lib/liquid2/context.rb', line 270

def raise_for_output_limit(length)
  return unless @env.output_stream_limit && (@env.output_stream_limit || raise) < length

  raise LiquidResourceLimitError, "output limit reached"
end

#resolve(name) ⇒ Object? Also known as: []

Resolve variable name in the current scope.

Parameters:

  • name (String)

Returns:

  • (Object?)


158
# File 'lib/liquid2/context.rb', line 158

def resolve(name) = @scope.fetch(name)

#stop_index(key, index: nil) ⇒ Object

Get or set the stop index of a for loop.



252
253
254
255
256
257
258
# File 'lib/liquid2/context.rb', line 252

def stop_index(key, index: nil)
  if index
    @tag_namespace[:stop_index][key] = index
  else
    @tag_namespace[:stop_index].fetch(key, 0)
  end
end