liquid2 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -21,16 +21,19 @@ require_relative "nodes/tags/cycle"
21
21
  require_relative "nodes/tags/decrement"
22
22
  require_relative "nodes/tags/doc"
23
23
  require_relative "nodes/tags/echo"
24
+ require_relative "nodes/tags/extends"
24
25
  require_relative "nodes/tags/for"
25
26
  require_relative "nodes/tags/if"
26
27
  require_relative "nodes/tags/include"
27
28
  require_relative "nodes/tags/increment"
28
29
  require_relative "nodes/tags/inline_comment"
29
30
  require_relative "nodes/tags/liquid"
31
+ require_relative "nodes/tags/macro"
30
32
  require_relative "nodes/tags/raw"
31
33
  require_relative "nodes/tags/render"
32
34
  require_relative "nodes/tags/tablerow"
33
35
  require_relative "nodes/tags/unless"
36
+ require_relative "nodes/tags/with"
34
37
 
35
38
  module Liquid2
36
39
  # Template parsing and rendering configuration.
@@ -42,8 +45,28 @@ module Liquid2
42
45
  class Environment
43
46
  attr_reader :tags, :local_namespace_limit, :context_depth_limit, :loop_iteration_limit,
44
47
  :output_stream_limit, :filters, :suppress_blank_control_flow_blocks,
45
- :shorthand_indexes
48
+ :shorthand_indexes, :falsy_undefined
46
49
 
50
+ # @param context_depth_limit [Integer] The maximum number of times a render context can
51
+ # be extended or copied before a `Liquid2::LiquidResourceLimitError`` is raised.
52
+ # @param globals [Hash[String, untyped]?] Variables that are available to all templates
53
+ # rendered from this environment.
54
+ # @param loader [Liquid2::Loader] An instance of `Liquid2::Loader`. A template loader
55
+ # is responsible for finding and reading templates for `{% include %}` and
56
+ # `{% render %}` tags, or when calling `Liquid2::Environment.get_template(name)`.
57
+ # @param local_namespace_limit [Integer?] The maximum allowed "size" of the template
58
+ # local namespace (variables from `assign` and `capture` tags) before a
59
+ # `Liquid2::LiquidResourceLimitError`` is raised.
60
+ # @param loop_iteration_limit [Integer?] The maximum number of loop iterations allowed
61
+ # before a `LiquidResourceLimitError` is raised.
62
+ # @param output_stream_limit [Integer?] The maximum number of bytes that can be written
63
+ # to a template's output buffer before a `LiquidResourceLimitError` is raised.
64
+ # @param shorthand_indexes [bool] When `true`, allow shorthand dotted array indexes as
65
+ # well as bracketed indexes in variable paths. Defaults to `false`.
66
+ # @param suppress_blank_control_flow_blocks [bool] When `true`, suppress blank control
67
+ # flow block output, so as not to include unnecessary whitespace. Defaults to `true`.
68
+ # @param undefined [singleton(Liquid2::Undefined)] A singleton returning an instance of
69
+ # `Liquid2::Undefined`, which is used to represent template variables that don't exist.
47
70
  def initialize(
48
71
  context_depth_limit: 30,
49
72
  globals: nil,
@@ -53,7 +76,8 @@ module Liquid2
53
76
  output_stream_limit: nil,
54
77
  shorthand_indexes: false,
55
78
  suppress_blank_control_flow_blocks: true,
56
- undefined: Undefined
79
+ undefined: Undefined,
80
+ falsy_undefined: true
57
81
  )
58
82
  # A mapping of tag names to objects responding to `parse(token, parser)`.
59
83
  @tags = {}
@@ -99,10 +123,14 @@ module Liquid2
99
123
  # unnecessary whitespace. Defaults to `true`.
100
124
  @suppress_blank_control_flow_blocks = suppress_blank_control_flow_blocks
101
125
 
102
- # An instance of `Liquid2::Undefined` used to represent template variables that
103
- # don't exist.
126
+ # A singleton returning an instance of `Liquid2::Undefined`, which is used to
127
+ # represent template variables that don't exist.
104
128
  @undefined = undefined
105
129
 
130
+ # When `true` (the default), undefined variables are considered falsy and do not
131
+ # raise an error when tested for truthiness.
132
+ @falsy_undefined = falsy_undefined
133
+
106
134
  # Override `setup_tags_and_filters` in environment subclasses to configure custom
107
135
  # tags and/or filters.
108
136
  setup_tags_and_filters
@@ -118,8 +146,8 @@ module Liquid2
118
146
  name: name, path: path, up_to_date: up_to_date,
119
147
  globals: make_globals(globals), overlay: overlay)
120
148
  rescue LiquidError => e
121
- e.source = source
122
- e.template_name = name unless name.empty?
149
+ e.source = source unless e.source
150
+ e.template_name = name unless e.template_name || name.empty?
123
151
  raise
124
152
  end
125
153
 
@@ -155,6 +183,20 @@ module Liquid2
155
183
  @filters.delete(name)
156
184
  end
157
185
 
186
+ # Add or replace a tag.
187
+ # @param name [String] The tag's name, as used by template authors.
188
+ # @param tag [responds to parse: ([Symbol, String?, Integer], Parser) -> Tag]
189
+ def register_tag(name, tag)
190
+ @tags[name] = tag
191
+ end
192
+
193
+ # Remove a tag from the tag register.
194
+ # @param name [String] The name of the tag.
195
+ # @return [_Tag | nil]
196
+ def delete_tag(name)
197
+ @tags.delete(name)
198
+ end
199
+
158
200
  def setup_tags_and_filters
159
201
  @tags["#"] = InlineComment
160
202
  @tags["assign"] = AssignTag
@@ -167,15 +209,20 @@ module Liquid2
167
209
  @tags["decrement"] = DecrementTag
168
210
  @tags["doc"] = DocTag
169
211
  @tags["echo"] = EchoTag
212
+ @tags["extends"] = ExtendsTag
213
+ @tags["block"] = BlockTag
170
214
  @tags["for"] = ForTag
171
215
  @tags["if"] = IfTag
172
216
  @tags["include"] = IncludeTag
173
217
  @tags["increment"] = IncrementTag
174
218
  @tags["liquid"] = LiquidTag
219
+ @tags["macro"] = MacroTag
220
+ @tags["call"] = CallTag
175
221
  @tags["raw"] = RawTag
176
222
  @tags["render"] = RenderTag
177
223
  @tags["tablerow"] = TableRowTag
178
224
  @tags["unless"] = UnlessTag
225
+ @tags["with"] = WithTag
179
226
 
180
227
  register_filter("abs", Liquid2::Filters.method(:abs))
181
228
  register_filter("append", Liquid2::Filters.method(:append))
@@ -7,10 +7,10 @@ module Liquid2
7
7
 
8
8
  FULL_MESSAGE = ((RUBY_VERSION.split(".")&.map(&:to_i) <=> [3, 2, 0]) || -1) < 1
9
9
 
10
- def initialize(message, token = nil)
10
+ def initialize(message, token = nil, template_name: nil)
11
11
  super(message)
12
12
  @token = token
13
- @template_name = nil
13
+ @template_name = template_name
14
14
  @source = nil
15
15
  end
16
16
 
@@ -76,4 +76,6 @@ module Liquid2
76
76
  class LiquidResourceLimitError < LiquidError; end
77
77
  class UndefinedError < LiquidError; end
78
78
  class DisabledTagError < LiquidError; end
79
+ class TemplateInheritanceError < LiquidError; end
80
+ class RequiredBlockError < TemplateInheritanceError; end
79
81
  end
@@ -22,4 +22,24 @@ module Liquid2
22
22
 
23
23
  def children = [@value]
24
24
  end
25
+
26
+ # A macro parameter with a name and optional default value.
27
+ class Parameter < Expression
28
+ attr_reader :value, :name, :sym
29
+
30
+ # @param name [String]
31
+ # @param value [Expression?]
32
+ def initialize(token, name, value)
33
+ super(token)
34
+ @name = name
35
+ @sym = name.to_sym
36
+ @value = value
37
+ end
38
+
39
+ def evaluate(context)
40
+ [@name, context.evaluate(@value)]
41
+ end
42
+
43
+ def children = [@value]
44
+ end
25
45
  end
@@ -12,7 +12,8 @@ module Liquid2
12
12
  end
13
13
 
14
14
  def evaluate(context)
15
- Liquid2.truthy?(context, context.evaluate(@expr))
15
+ value = context.evaluate(@expr)
16
+ Liquid2.truthy?(context, value.respond_to?(:to_liquid) ? value.to_liquid(context) : value)
16
17
  end
17
18
 
18
19
  def children = [@expr]
@@ -15,8 +15,10 @@ module Liquid2
15
15
 
16
16
  def evaluate(context)
17
17
  left = context.evaluate(@left)
18
+ return left if @filters.nil?
19
+
18
20
  index = 0
19
- while (filter = @filters[index])
21
+ while (filter = (@filters || raise)[index])
20
22
  left = filter.evaluate(left, context)
21
23
  index += 1
22
24
  end
@@ -81,7 +83,7 @@ module Liquid2
81
83
  attr_reader :name, :args
82
84
 
83
85
  # @param name [String]
84
- # @param args [Array[Expression]]
86
+ # @param args [Array[Expression]?]
85
87
  def initialize(token, name, args)
86
88
  super(token)
87
89
  @name = name
@@ -92,36 +94,18 @@ module Liquid2
92
94
  filter, with_context = context.env.filters[@name]
93
95
  raise LiquidFilterNotFoundError.new("unknown filter #{@name.inspect}", @token) unless filter
94
96
 
95
- positional_args, keyword_args = evaluate_args(context)
96
- keyword_args[:context] = context if with_context
97
-
98
- if keyword_args.empty?
99
- filter.call(left, *positional_args) # steep:ignore
100
- else
101
- filter.call(left, *positional_args, **keyword_args) # steep:ignore
102
- end
103
- rescue ArgumentError, TypeError => e
104
- raise LiquidArgumentError.new(e.message, @token)
105
- end
106
-
107
- def children = @args
97
+ return filter.call(left) if @args.nil? && !with_context # steep:ignore
98
+ return filter.call(left, context: context) if @args.nil? && with_context # steep:ignore
108
99
 
109
- private
110
-
111
- # @param context [RenderContext]
112
- # @return [positional arguments, keyword arguments] An array with two elements.
113
- # The first is an array of evaluates positional arguments. The second is a hash
114
- # of keyword names to evaluated keyword values.
115
- def evaluate_args(context)
116
100
  positional_args = [] # @type var positional_args: Array[untyped]
117
101
  keyword_args = {} # @type var keyword_args: Hash[Symbol, untyped]
118
102
 
119
103
  index = 0
120
104
  loop do
121
105
  # `@args[index]` could be `false` or `nil`
122
- break if index >= @args.length
106
+ break if index >= @args.length # steep:ignore
123
107
 
124
- arg = @args[index]
108
+ arg = @args[index] # steep:ignore
125
109
  index += 1
126
110
  if arg.respond_to?(:sym)
127
111
  keyword_args[arg.sym] = context.evaluate(arg.value)
@@ -130,7 +114,17 @@ module Liquid2
130
114
  end
131
115
  end
132
116
 
133
- [positional_args, keyword_args]
117
+ keyword_args[:context] = context if with_context
118
+
119
+ if keyword_args.empty?
120
+ filter.call(left, *positional_args) # steep:ignore
121
+ else
122
+ filter.call(left, *positional_args, **keyword_args) # steep:ignore
123
+ end
124
+ rescue ArgumentError, TypeError => e
125
+ raise LiquidArgumentError.new(e.message, @token)
134
126
  end
127
+
128
+ def children = @args || []
135
129
  end
136
130
  end
@@ -33,11 +33,13 @@ module Liquid2
33
33
  elsif obj.is_a?(String)
34
34
  # TODO: optionally enable/disable string iteration
35
35
  obj.each_char.to_a
36
- elsif obj.respond_to?(:each)
37
- # TODO: special lazy drop slicing
38
- # #each and #slice is our enumerable drop interface
39
- # TODO: or just #to_a
40
- obj.each.to_a
36
+ elsif obj.respond_to?(:slice)
37
+ # Special lazy drop slicing
38
+ return obj.slice(context.evaluate(@offset),
39
+ context.evaluate(@limit),
40
+ @reversed) || EMPTY_ENUM
41
+ elsif obj.is_a?(Enumerable) # rubocop:disable Lint/DuplicateBranch
42
+ obj.to_a
41
43
  else
42
44
  EMPTY_ENUM
43
45
  end
@@ -8,6 +8,8 @@ module Liquid2
8
8
  class Path < Expression
9
9
  attr_reader :segments, :head
10
10
 
11
+ RE_PROPERTY = /\A[\u0080-\uFFFFa-zA-Z_][\u0080-\uFFFFa-zA-Z0-9_-]*\Z/
12
+
11
13
  # @param segments [Array[String | Integer | Path]]
12
14
  def initialize(token, segments)
13
15
  super(token)
@@ -19,8 +21,9 @@ module Liquid2
19
21
  context.fetch(@head, @segments, node: self)
20
22
  end
21
23
 
22
- # TODO: fix and optimize (store it on the instance)
23
- def to_s = "#{@head}.#{@segments.map(&:to_s).join(".")}"
24
+ def to_s
25
+ segment_to_s(@head, head: true) + @segments.map { |segment| segment_to_s(segment) }.join
26
+ end
24
27
 
25
28
  def children
26
29
  if @head.is_a?(Path)
@@ -29,5 +32,19 @@ module Liquid2
29
32
  @segments.filter { |segment| segment.is_a?(Path) }
30
33
  end
31
34
  end
35
+
36
+ private
37
+
38
+ def segment_to_s(segment, head: false)
39
+ if segment.is_a?(String)
40
+ if segment.match?(RE_PROPERTY)
41
+ "#{head ? "" : "."}#{segment}"
42
+ else
43
+ "[#{segment.inspect}]"
44
+ end
45
+ else
46
+ "[#{segment}]"
47
+ end
48
+ end
32
49
  end
33
50
  end
@@ -63,8 +63,7 @@ module Liquid2
63
63
  end
64
64
 
65
65
  # Cast _obj_ to a date and time. Return `nil` if casting fails.
66
- #
67
- # TODO: This was copied from Shopify/liquid. Include their license and copyright.
66
+ # NOTE: This was copied from Shopify/liquid.
68
67
  def self.to_date(obj)
69
68
  return obj if obj.respond_to?(:strftime)
70
69
 
@@ -29,7 +29,6 @@ module Liquid2
29
29
  when :undefined
30
30
  left.compact
31
31
  else
32
- # TODO: stringify key?
33
32
  left.reject do |item|
34
33
  item.respond_to?(:fetch) ? item.fetch(key, nil).nil? : true
35
34
  end
@@ -3,6 +3,8 @@
3
3
  module Liquid2
4
4
  # Liquid filters and helper methods.
5
5
  module Filters
6
+ INFINITY_ARRAY = [Float::INFINITY].freeze # : [Float]
7
+
6
8
  def self.sort(left, key = nil, context:)
7
9
  left = Liquid2::Filters.to_enumerable(left)
8
10
 
@@ -82,12 +84,11 @@ module Liquid2
82
84
  end
83
85
 
84
86
  def self.ints(obj)
85
- case obj
86
- when Integer, Float, BigDecimal
87
+ if obj.is_a?(Integer) || obj.is_a?(Float) || obj.is_a?(BigDecimal)
87
88
  [obj]
88
89
  else
89
- numeric = obj.to_s.scan(/-?\d+/)
90
- return [Float::INFINITY] if numeric.empty?
90
+ numeric = obj.to_s.scan(/(?<=\.)0+|-?\d+/)
91
+ return INFINITY_ARRAY if numeric.empty?
91
92
 
92
93
  numeric.map(&:to_i)
93
94
  end
@@ -31,6 +31,7 @@ module Liquid2
31
31
  def load(env, name, globals: nil, context: nil, **kwargs)
32
32
  data = get_source(env, name, context: context, **kwargs)
33
33
  path = Pathname.new(data.name)
34
+ # FIXME: name and path
34
35
  env.parse(data.source,
35
36
  name: path.basename.to_s,
36
37
  path: data.name,
@@ -5,6 +5,8 @@ require_relative "../../tag"
5
5
  module Liquid2
6
6
  # The standard _doc_ tag.
7
7
  class DocTag < Tag
8
+ attr_reader :text
9
+
8
10
  def self.parse(token, parser)
9
11
  parser.carry_whitespace_control
10
12
  parser.eat(:token_tag_end)
@@ -1,3 +1,272 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # TODO
3
+ require_relative "../../tag"
4
+
5
+ module Liquid2
6
+ # The _extends_ tag.
7
+ class ExtendsTag < Tag
8
+ attr_reader :template_name
9
+
10
+ # @param token [[Symbol, String?, Integer]]
11
+ # @param parser [Parser]
12
+ # @return [ExtendsTag]
13
+ def self.parse(token, parser)
14
+ name = parser.parse_name
15
+ parser.carry_whitespace_control
16
+ parser.eat(:token_tag_end)
17
+ new(token, name)
18
+ end
19
+
20
+ def initialize(token, name)
21
+ super(token)
22
+ @template_name = name
23
+ @blank = false
24
+ end
25
+
26
+ def render(context, buffer)
27
+ base_template = stack_blocks(context, context.template)
28
+ context.extend({}, template: base_template) do
29
+ base_template&.render_with_context(context, buffer)
30
+ end
31
+ context.tag_namespace[:extends].clear
32
+ context.interrupts << :stop_render
33
+ end
34
+
35
+ def children(static_context, include_partials: true)
36
+ return [] unless include_partials
37
+
38
+ begin
39
+ parent = static_context.env.get_template(
40
+ @template_name,
41
+ context: static_context,
42
+ tag: "extends"
43
+ )
44
+ parent.ast
45
+ rescue LiquidTemplateNotFoundError => e
46
+ e.token = @token
47
+ e.template_name = static_context.template.full_name
48
+ raise e
49
+ end
50
+ end
51
+
52
+ def partial_scope
53
+ Partial.new(@template_name, :inherited, [])
54
+ end
55
+
56
+ protected
57
+
58
+ # Visit all templates in the inheritance chain and build a stack for each `block` tag.
59
+ def stack_blocks(context, template)
60
+ # @type var stacks: Hash[String, Array[untyped]]
61
+ stacks = context.tag_namespace[:extends]
62
+
63
+ # Guard against recursive `extends`.
64
+ seen_extends = Set[] # : Set[String]
65
+
66
+ # @type var stack_blocks_: ^(Template) -> Template?
67
+ stack_blocks_ = lambda do |template_|
68
+ extends_nodes, block_nodes = inheritance_nodes(context, template_)
69
+ template_name = template_.path || template_.name
70
+
71
+ if extends_nodes.length > 1
72
+ raise TemplateInheritanceError.new("too many 'extends' tags", extends_nodes[1].token,
73
+ template_name: template_name)
74
+ end
75
+
76
+ # Identify duplicate blocks.
77
+ seen_blocks = Set[] # : Set[String]
78
+
79
+ block_nodes.each do |block|
80
+ if seen_blocks.include?(block.block_name)
81
+ raise TemplateInheritanceError.new("duplicate block #{block.block_name.inspect}",
82
+ block.token, template_name: template_name)
83
+ end
84
+
85
+ seen_blocks.add(block.block_name)
86
+
87
+ stack = stacks[block.block_name]
88
+ required = !stack.empty? && !block.required ? false : block.required
89
+ # [block, required, template, parent]
90
+ # [BlockTag, bool, Template, untyped?]
91
+ stack << [block, required, template_, nil]
92
+ # Populate parent block.
93
+ stack[-2][-1] = stack.last if stack.length > 1
94
+ end
95
+
96
+ return nil if extends_nodes.empty? # steep:ignore
97
+
98
+ extends_node = extends_nodes.first
99
+
100
+ if seen_extends.include?(extends_node.template_name)
101
+ raise TemplateInheritanceError.new(
102
+ "circular extends #{extends_node.template_name.inspect}",
103
+ extends_node.token,
104
+ template_name: template_name
105
+ )
106
+ end
107
+
108
+ seen_extends.add(extends_node.template_name)
109
+
110
+ begin
111
+ context.env.get_template(extends_node.template_name, context: context, tag: "extends")
112
+ rescue LiquidTemplateNotFoundError => e
113
+ e.token = extends_node.token
114
+ e.template_name = template_.full_name
115
+ raise e
116
+ end
117
+ end
118
+
119
+ # @type var next_template: Template?
120
+ base = next_template = stack_blocks_.call(template)
121
+
122
+ while next_template
123
+ next_template = stack_blocks_.call(next_template)
124
+ base = next_template if next_template
125
+ end
126
+
127
+ base
128
+ end
129
+
130
+ # Traverse the template's syntax tree looking for `{% extends %}` and `{% block %}`.
131
+ # @return [[Array[ExtendsTag], Array[BlockTag]]]
132
+ def inheritance_nodes(context, template)
133
+ extends_nodes = [] # : Array[ExtendsTag]
134
+ block_nodes = [] # : Array[BlockTag]
135
+
136
+ # @type var visit: ^(Node) -> void
137
+ visit = lambda do |node|
138
+ extends_nodes << node if node.is_a?(ExtendsTag)
139
+ block_nodes << node if node.is_a?(BlockTag)
140
+
141
+ node.children(context, include_partials: false).each do |child|
142
+ visit.call(child) if child.is_a?(Node)
143
+ end
144
+ end
145
+
146
+ template.ast.each { |node| visit.call(node) if node.is_a?(Node) }
147
+
148
+ [extends_nodes, block_nodes]
149
+ end
150
+ end
151
+
152
+ # The _block_ tag.
153
+ class BlockTag < Tag
154
+ attr_reader :block_name, :required, :block
155
+
156
+ END_BLOCK = Set["endblock"]
157
+
158
+ # @param token [[Symbol, String?, Integer]]
159
+ # @param parser [Parser]
160
+ # @return [BlockTag]
161
+ def self.parse(token, parser)
162
+ block_name = parser.parse_name
163
+ required = if parser.current_kind == :token_required
164
+ parser.next
165
+ true
166
+ else
167
+ false
168
+ end
169
+
170
+ parser.carry_whitespace_control
171
+ parser.eat(:token_tag_end)
172
+ block = parser.parse_block(END_BLOCK)
173
+ parser.eat_empty_tag("endblock")
174
+ new(token, block_name, block, required: required)
175
+ end
176
+
177
+ def initialize(token, name, block, required:)
178
+ super(token)
179
+ @block_name = name
180
+ @block = block
181
+ @required = required
182
+ @blank = false
183
+ end
184
+
185
+ def render(context, buffer)
186
+ # @type var stack: Array[[BlockTag, bool, Template, untyped?]]
187
+ stack = context.tag_namespace[:extends][@block_name]
188
+
189
+ if stack.empty?
190
+ # This base block is being rendered directly.
191
+ if @required
192
+ raise RequiredBlockError.new("block #{@block_name.inspect} is required",
193
+ @token)
194
+ end
195
+
196
+ context.extend({ "block" => BlockDrop.new(token, context, @block_name, nil) }) do
197
+ @block.render(context, buffer)
198
+ end
199
+
200
+ return
201
+ end
202
+
203
+ block_tag, required, template, parent = stack.first
204
+
205
+ if required
206
+ raise RequiredBlockError.new("block #{@block_name.inspect} is required", @token,
207
+ template_name: template.path || template.name)
208
+ end
209
+
210
+ namespace = { "block" => BlockDrop.new(token, context, @block_name, parent) }
211
+
212
+ block_context = context.copy(namespace,
213
+ carry_loop_iterations: true,
214
+ block_scope: true,
215
+ template: template)
216
+
217
+ begin
218
+ block_tag.block.render(block_context, buffer)
219
+ rescue LiquidError => e
220
+ e.template_name = template.path || template.name
221
+ e.source = template.source
222
+ raise
223
+ end
224
+ end
225
+
226
+ def children(_static_context, include_partials: true)
227
+ [@block]
228
+ end
229
+
230
+ def block_scope
231
+ [Identifier.new([:token_word, "block", @token.last])]
232
+ end
233
+ end
234
+
235
+ # A `block` object available within `{% block %}` tags.
236
+ class BlockDrop
237
+ attr_reader :token
238
+
239
+ # @param token [[Symbol, String?, Integer]]
240
+ # @param context [RenderContext]
241
+ # @param name [String]
242
+ # @param parent [[BlockTag, bool, Template, Block?]?]
243
+ def initialize(token, context, name, parent)
244
+ @token = token
245
+ @context = context
246
+ @name = name
247
+ @parent = parent
248
+ end
249
+
250
+ def to_s = "BlockDrop(#{@name})"
251
+
252
+ def key?(key)
253
+ key == "super" && @parent
254
+ end
255
+
256
+ def [](key)
257
+ return nil if key != "super" || @parent.nil?
258
+
259
+ parent = @parent || raise
260
+ buf = +""
261
+ namespace = { "block" => BlockDrop.new(parent.first.token,
262
+ @context,
263
+ parent[2].path || parent[2].name,
264
+ parent.last) }
265
+ @context.extend(namespace) do
266
+ parent.first.block.render(@context, buf)
267
+ end
268
+
269
+ buf.freeze
270
+ end
271
+ end
272
+ end