liquid2 0.1.1 → 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.
@@ -47,7 +47,7 @@ module Liquid2
47
47
  # @param args [Array<KeywordArgument> | nil]
48
48
  def initialize(token, name, repeat, var, as, args)
49
49
  super(token)
50
- @name = name
50
+ @template_name = name
51
51
  @repeat = repeat
52
52
  @var = var
53
53
  @as = as
@@ -56,7 +56,7 @@ module Liquid2
56
56
  end
57
57
 
58
58
  def render(context, buffer)
59
- name = context.evaluate(@name)
59
+ name = context.evaluate(@template_name)
60
60
  template = context.env.get_template(name.to_s, context: context, tag: :include)
61
61
  namespace = @args.to_h { |arg| [arg.name, context.evaluate(arg.value)] }
62
62
 
@@ -90,7 +90,7 @@ module Liquid2
90
90
  def children(static_context, include_partials: true)
91
91
  return [] unless include_partials
92
92
 
93
- name = static_context.evaluate(@name)
93
+ name = static_context.evaluate(@template_name)
94
94
  template = static_context.env.get_template(name.to_s, context: static_context, tag: :include)
95
95
  template.ast
96
96
  rescue LiquidTemplateNotFoundError => e
@@ -100,7 +100,7 @@ module Liquid2
100
100
  end
101
101
 
102
102
  def expressions
103
- exprs = [@name]
103
+ exprs = [@template_name]
104
104
  exprs << @var if @var
105
105
  exprs.concat(@args.map(&:value))
106
106
  exprs
@@ -112,12 +112,12 @@ module Liquid2
112
112
  if @var
113
113
  if @as
114
114
  scope << @as # steep:ignore
115
- elsif @name.is_a?(String)
116
- scope << Identifier.new([:token_word, @name.split(".").first, @token.last])
115
+ elsif @template_name.is_a?(String)
116
+ scope << Identifier.new([:token_word, @template_name.split(".").first, @token.last])
117
117
  end
118
118
  end
119
119
 
120
- Partial.new(@name, :shared, scope)
120
+ Partial.new(@template_name, :shared, scope)
121
121
  end
122
122
  end
123
123
  end
@@ -1,3 +1,147 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # TODO
3
+ require_relative "../../tag"
4
+
5
+ module Liquid2
6
+ # The _macro_ tag.
7
+ class MacroTag < Tag
8
+ attr_reader :macro_name, :params, :block
9
+
10
+ END_BLOCK = Set["endmacro"]
11
+
12
+ # @param token [[Symbol, String?, Integer]]
13
+ # @param parser [Parser]
14
+ # @return [MacroTag]
15
+ def self.parse(token, parser)
16
+ name = parser.parse_name
17
+ parser.next if parser.current_kind == :token_comma
18
+ params = parser.parse_parameters
19
+ parser.next if parser.current_kind == :token_comma
20
+ parser.carry_whitespace_control
21
+ parser.eat(:token_tag_end)
22
+ block = parser.parse_block(END_BLOCK)
23
+ parser.eat_empty_tag("endmacro")
24
+ new(token, name, params, block)
25
+ end
26
+
27
+ def initialize(token, name, params, block)
28
+ super(token)
29
+ @macro_name = name
30
+ @params = params
31
+ @block = block
32
+ @blank = true
33
+ end
34
+
35
+ def render(context, _buffer)
36
+ # Macro tags don't render or evaluate anything, just store their parameter list
37
+ # and block on the render context so it can be called later by a `call` tag.
38
+ context.tag_namespace[:macros][@macro_name] = [@params, @block]
39
+ end
40
+
41
+ def children(_static_context, include_partials: true) = [@block]
42
+ def expressions = @params.values.filter_map(&:value)
43
+
44
+ def block_scope
45
+ [
46
+ Identifier.new([:token_word, "args", @token.last]),
47
+ Identifier.new([:token_word, "kwargs", @token.last]),
48
+ *@params.values.map { |param| Identifier.new([:token_word, param.name, param.token.last]) }
49
+ ]
50
+ end
51
+ end
52
+
53
+ # The _call_ tag.
54
+ class CallTag < Tag
55
+ attr_reader :macro_name, :args, :kwargs
56
+
57
+ DISABLED_TAGS = Set["include", "block"]
58
+
59
+ # @param token [[Symbol, String?, Integer]]
60
+ # @param parser [Parser]
61
+ # @return [CallTag]
62
+ def self.parse(token, parser)
63
+ name = parser.parse_name
64
+ parser.next if parser.current_kind == :token_comma
65
+ args, kwargs = parser.parse_arguments
66
+ parser.carry_whitespace_control
67
+ parser.eat(:token_tag_end)
68
+ new(token, name, args, kwargs)
69
+ end
70
+
71
+ def initialize(token, name, args, kwargs)
72
+ super(token)
73
+ @macro_name = name
74
+ @args = args
75
+ @kwargs = kwargs
76
+ @blank = false
77
+ end
78
+
79
+ def render(context, buffer)
80
+ # @type var params: Hash[String, Parameter]?
81
+ # @type var block: Block
82
+ params, block = context.tag_namespace[:macros][@macro_name]
83
+
84
+ unless params
85
+ buffer << Liquid2.to_output_string(context.env.undefined(@macro_name, node: self))
86
+ return
87
+ end
88
+
89
+ # Parameter names mapped to default values. :undefined is used if there is no default.
90
+ args = params.values.to_h { |p| [p.name, p.value] }
91
+ excess_args = [] # : Array[untyped]
92
+ excess_kwargs = {} # : Hash[String, untyped]
93
+
94
+ # Update args with positional arguments.
95
+ # Keyword arguments are pushed to the end if they appear before positional arguments.
96
+ names = args.keys
97
+ length = @args.length
98
+ index = 0
99
+ while index < length
100
+ name = names[index]
101
+ expr = @args[index]
102
+ if name.nil?
103
+ excess_args << expr
104
+ else
105
+ args[name] = expr
106
+ end
107
+ index += 1
108
+ end
109
+
110
+ # Update args with keyword arguments.
111
+ @kwargs.each do |arg|
112
+ if params.include?(arg.name)
113
+ # This has the potential to override a positional argument.
114
+ args[arg.name] = arg.value
115
+ else
116
+ excess_kwargs[arg.name] = arg.value
117
+ end
118
+ end
119
+
120
+ # @type var namespace: Hash[String, untyped]
121
+ namespace = {
122
+ "args" => excess_args.map { |arg| context.evaluate(arg) },
123
+ "kwargs" => excess_kwargs.transform_values! { |val| context.evaluate(val) }
124
+ }
125
+
126
+ args.each do |k, v|
127
+ namespace[k] = if v == :undefined
128
+ context.env.undefined(k, node: params[k])
129
+ else
130
+ context.evaluate(v)
131
+ end
132
+ end
133
+
134
+ macro_context = context.copy(
135
+ namespace,
136
+ disabled_tags: DISABLED_TAGS,
137
+ carry_loop_iterations: true
138
+ )
139
+
140
+ block.render(macro_context, buffer)
141
+ end
142
+
143
+ def expressions
144
+ [*@args, *@kwargs.map(&:value)]
145
+ end
146
+ end
147
+ end
@@ -12,8 +12,6 @@ module Liquid2
12
12
  # @return [RenderTag]
13
13
  def self.parse(token, parser)
14
14
  name = parser.parse_string
15
- raise LiquidTypeError, "expected a string literal" unless name.is_a?(String)
16
-
17
15
  repeat = false
18
16
  var = nil # : Expression?
19
17
  as = nil # : Identifier?
@@ -51,7 +49,7 @@ module Liquid2
51
49
  # @param args [Array<KeywordArgument> | nil]
52
50
  def initialize(token, name, repeat, var, as, args)
53
51
  super(token)
54
- @name = name
52
+ @template_name = name
55
53
  @repeat = repeat
56
54
  @var = var
57
55
  @as = as&.name
@@ -60,7 +58,7 @@ module Liquid2
60
58
  end
61
59
 
62
60
  def render(context, buffer)
63
- template = context.env.get_template(@name, context: context, tag: :render)
61
+ template = context.env.get_template(@template_name, context: context, tag: :render)
64
62
  namespace = @args.to_h { |arg| [arg.name, context.evaluate(arg.value)] }
65
63
 
66
64
  ctx = context.copy(namespace,
@@ -96,7 +94,7 @@ module Liquid2
96
94
  template.render_with_context(ctx, buffer, partial: true, block_scope: true)
97
95
  end
98
96
  rescue LiquidTemplateNotFoundError => e
99
- e.token = @name
97
+ e.token = @template_name
100
98
  e.template_name = context.template.full_name
101
99
  raise e
102
100
  end
@@ -104,7 +102,7 @@ module Liquid2
104
102
  def children(static_context, include_partials: true)
105
103
  return [] unless include_partials
106
104
 
107
- name = static_context.evaluate(@name)
105
+ name = static_context.evaluate(@template_name)
108
106
  template = static_context.env.get_template(name.to_s, context: static_context, tag: :include)
109
107
  template.ast
110
108
  rescue LiquidTemplateNotFoundError => e
@@ -114,7 +112,7 @@ module Liquid2
114
112
  end
115
113
 
116
114
  def expressions
117
- exprs = [@name]
115
+ exprs = [@template_name]
118
116
  exprs << @var if @var
119
117
  exprs.concat(@args.map(&:value))
120
118
  exprs
@@ -126,12 +124,12 @@ module Liquid2
126
124
  if @var
127
125
  if @as
128
126
  scope << @as # steep:ignore
129
- elsif @name.is_a?(String)
130
- scope << Identifier.new([:token_word, @name.split(".").first, @token.last])
127
+ elsif @template_name.is_a?(String)
128
+ scope << Identifier.new([:token_word, @template_name.split(".").first, @token.last])
131
129
  end
132
130
  end
133
131
 
134
- Partial.new(@name, :isolated, scope)
132
+ Partial.new(@template_name, :isolated, scope)
135
133
  end
136
134
  end
137
135
  end
@@ -1,3 +1,44 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # TODO
3
+ require_relative "../../tag"
4
+
5
+ module Liquid2
6
+ # The _with_ tag.
7
+ class WithTag < Tag
8
+ END_BLOCK = Set["endwith"]
9
+
10
+ # @param token [[Symbol, String?, Integer]]
11
+ # @param parser [Parser]
12
+ # @return [WithTag]
13
+ def self.parse(token, parser)
14
+ parser.next if parser.current_kind == :token_comma
15
+ args = parser.parse_keyword_arguments
16
+ parser.next if parser.current_kind == :token_comma
17
+ parser.carry_whitespace_control
18
+ parser.eat(:token_tag_end)
19
+ block = parser.parse_block(END_BLOCK)
20
+ parser.eat_empty_tag("endwith")
21
+ new(token, args, block)
22
+ end
23
+
24
+ # @param token [[Symbol, String?, Integer]]
25
+ # @param args [Array[KeywordArgument]]
26
+ # @param block [Block]
27
+ def initialize(token, args, block)
28
+ super(token)
29
+ @args = args
30
+ @block = block
31
+ @blank = block.blank
32
+ end
33
+
34
+ def render(context, buffer)
35
+ namespace = @args.to_h { |arg| [arg.name, context.evaluate(arg.value)] }
36
+ context.extend(namespace) do
37
+ @block.render(context, buffer)
38
+ end
39
+ end
40
+
41
+ def children(_static_context, include_partials: true) = [@block]
42
+ def block_scope = @args.map { |arg| Identifier.new(arg.token) }
43
+ end
44
+ end
@@ -233,7 +233,6 @@ module Liquid2
233
233
  left = parse_primary
234
234
  left = parse_array_literal(left) if current_kind == :token_comma
235
235
  filters = parse_filters if current_kind == :token_pipe
236
- filters ||= [] # : Array[Filter]
237
236
  expr = FilteredExpression.new(token, left, filters)
238
237
 
239
238
  if current_kind == :token_if
@@ -396,7 +395,7 @@ module Liquid2
396
395
  # @raises [LiquidTypeError].
397
396
  def parse_string
398
397
  node = parse_primary
399
- raise LiquidTypeError, "expected a string" unless node.is_a?(String)
398
+ raise LiquidTypeError, "expected a string literal" unless node.is_a?(String)
400
399
 
401
400
  node
402
401
  end
@@ -411,6 +410,23 @@ module Liquid2
411
410
  Identifier.new(token)
412
411
  end
413
412
 
413
+ # Parse a string literals or unquoted word.
414
+ def parse_name
415
+ case current_kind
416
+ when :token_word
417
+ parse_identifier.name
418
+ when :token_single_quote_string, :token_double_quote_string
419
+ node = parse_string_literal
420
+ unless node.is_a?(String)
421
+ raise LiquidSyntaxError.new("names can't be template strings", node.token)
422
+ end
423
+
424
+ node
425
+ else
426
+ raise LiquidSyntaxError.new("expected a string literal or unquoted word", current)
427
+ end
428
+ end
429
+
414
430
  # Parse comma separated expression.
415
431
  # Leading commas should be consumed by the caller.
416
432
  # @return [Array<Expression>]
@@ -427,7 +443,7 @@ module Liquid2
427
443
  args
428
444
  end
429
445
 
430
- # Parse comma name/value pairs.
446
+ # Parse comma separated name/value pairs.
431
447
  # Leading commas should be consumed by the caller, if allowed.
432
448
  # @return [Array<KeywordArgument>]
433
449
  def parse_keyword_arguments
@@ -438,8 +454,7 @@ module Liquid2
438
454
 
439
455
  word = eat(:token_word)
440
456
  eat_one_of(:token_assign, :token_colon)
441
- val = parse_primary
442
- args << KeywordArgument.new(word, word[1] || raise, val)
457
+ args << KeywordArgument.new(word, word[1] || raise, parse_primary)
443
458
 
444
459
  break unless current_kind == :token_comma
445
460
 
@@ -449,6 +464,68 @@ module Liquid2
449
464
  args
450
465
  end
451
466
 
467
+ # Parse comma separated parameter names with optional default expressions.
468
+ # Leading commas should be consumed by the caller, if allowed.
469
+ # @return [Hash[String, Parameter]]
470
+ def parse_parameters
471
+ args = {} # : Hash[String, Parameter]
472
+
473
+ loop do
474
+ break if TERMINATE_EXPRESSION.member?(current_kind)
475
+
476
+ word = eat(:token_word)
477
+ name = word[1] || raise
478
+
479
+ case current_kind
480
+ when :token_assign, :token_colon
481
+ @pos += 1
482
+ args[name] = Parameter.new(word, name, parse_primary)
483
+ @pos += 1 if current_kind == :token_comma
484
+ when :comma
485
+ args[name] = Parameter.new(word, name, :undefined)
486
+ @pos += 1
487
+ else
488
+ args[name] = Parameter.new(word, name, :undefined)
489
+ break
490
+ end
491
+ end
492
+
493
+ args
494
+ end
495
+
496
+ # Parse mixed positional and keyword arguments.
497
+ # Leading commas should be consumed by the caller, if allowed.
498
+ # @return [[Array[untyped], Array[KeywordArgument]]]
499
+ def parse_arguments
500
+ args = [] # : Array[untyped]
501
+ kwargs = [] # : Array[KeywordArgument]
502
+
503
+ loop do
504
+ break if TERMINATE_EXPRESSION.member?(current_kind)
505
+
506
+ case current_kind
507
+ when :token_word
508
+ if KEYWORD_ARGUMENT_DELIMITERS.include?(peek_kind)
509
+ token = self.next
510
+ @pos += 1 # = or :
511
+ kwargs << KeywordArgument.new(token, token[1] || raise, parse_primary)
512
+ else
513
+ # A positional argument
514
+ args << parse_primary
515
+ end
516
+ else
517
+ # A positional argument
518
+ args << parse_primary
519
+ end
520
+
521
+ break unless current_kind == :token_comma
522
+
523
+ @pos += 1
524
+ end
525
+
526
+ [args, kwargs]
527
+ end
528
+
452
529
  protected
453
530
 
454
531
  class Precedence
@@ -783,7 +860,7 @@ module Liquid2
783
860
 
784
861
  unless current_kind == :token_colon || !TERMINATE_FILTER.member?(current_kind)
785
862
  # No arguments
786
- return Filter.new(name, name[1] || raise, []) # TODO: optimize
863
+ return Filter.new(name, name[1] || raise, nil)
787
864
  end
788
865
 
789
866
  @pos += 1 # token_colon
@@ -804,7 +881,7 @@ module Liquid2
804
881
  args << parse_arrow_function
805
882
  else
806
883
  # A positional argument that is a path.
807
- args << parse_path
884
+ args << parse_primary
808
885
  end
809
886
  when :token_lparen
810
887
  # A grouped expression or range or arrow function
@@ -12,8 +12,6 @@ module Liquid2
12
12
  class Scanner
13
13
  attr_reader :tokens
14
14
 
15
- RE_MARKUP_START = /\{[\{%#]/
16
- RE_WHITESPACE = /[ \n\r\t]+/
17
15
  RE_LINE_SPACE = /[ \t]+/
18
16
  RE_WORD = /[\u0080-\uFFFFa-zA-Z_][\u0080-\uFFFFa-zA-Z0-9_-]*/
19
17
  RE_INT = /-?\d+(?:[eE]\+?\d+)?/
@@ -94,31 +92,19 @@ module Liquid2
94
92
  # @param value [String?]
95
93
  # @return void
96
94
  def emit(kind, value)
97
- # TODO: For debugging. Comment this out when benchmarking.
98
- raise "empty span (#{kind}, #{value})" if @scanner.pos == @start
99
-
100
95
  @tokens << [kind, value, @start]
101
96
  @start = @scanner.pos
102
97
  end
103
98
 
104
99
  def skip_trivia
105
- # TODO: For debugging. Comment this out when benchmarking.
106
- raise "must emit before skipping trivia" if @scanner.pos != @start
107
-
108
- @start = @scanner.pos if @scanner.skip(RE_WHITESPACE)
100
+ @start = @scanner.pos if @scanner.skip(/[ \n\r\t]+/)
109
101
  end
110
102
 
111
103
  def skip_line_trivia
112
- # TODO: For debugging. Comment this out when benchmarking.
113
- raise "must emit before skipping line trivia" if @scanner.pos != @start
114
-
115
104
  @start = @scanner.pos if @scanner.skip(RE_LINE_SPACE)
116
105
  end
117
106
 
118
107
  def accept_whitespace_control
119
- # TODO: For debugging. Comment this out when benchmarking.
120
- raise "must emit before accepting whitespace control" if @scanner.pos != @start
121
-
122
108
  ch = @scanner.peek(1)
123
109
 
124
110
  case ch
@@ -133,7 +119,7 @@ module Liquid2
133
119
  end
134
120
 
135
121
  def lex_markup
136
- case @scanner.scan(RE_MARKUP_START)
122
+ case @scanner.scan(/\{[\{%#]/)
137
123
  when "{#"
138
124
  :lex_comment
139
125
  when "{{"
@@ -197,37 +183,30 @@ module Liquid2
197
183
  end
198
184
 
199
185
  def lex_expression
200
- # TODO: For debugging. Comment this out when benchmarking.
201
- raise "must emit before accepting an expression token" if @scanner.pos != @start
202
-
203
186
  loop do
204
187
  skip_trivia
205
-
206
- case @scanner.get_byte
207
- when "'"
188
+ if (value = @scanner.scan(RE_FLOAT))
189
+ @tokens << [:token_float, value, @start]
208
190
  @start = @scanner.pos
209
- scan_string("'", :token_single_quote_string, RE_SINGLE_QUOTE_STRING_SPECIAL)
210
- when "\""
191
+ elsif (value = @scanner.scan(RE_INT))
192
+ @tokens << [:token_int, value, @start]
193
+ @start = @scanner.pos
194
+ elsif (value = @scanner.scan(RE_PUNCTUATION))
195
+ @tokens << [TOKEN_MAP[value] || :token_unknown, value, @start]
196
+ @start = @scanner.pos
197
+ elsif (value = @scanner.scan(RE_WORD))
198
+ @tokens << [TOKEN_MAP[value] || :token_word, value, @start]
211
199
  @start = @scanner.pos
212
- scan_string("\"", :token_double_quote_string, RE_DOUBLE_QUOTE_STRING_SPECIAL)
213
- when nil
214
- # End of scanner. Unclosed expression or string literal.
215
- break
216
200
  else
217
- @scanner.pos -= 1
218
- if (value = @scanner.scan(RE_FLOAT))
219
- @tokens << [:token_float, value, @start]
201
+ case @scanner.get_byte
202
+ when "'"
220
203
  @start = @scanner.pos
221
- elsif (value = @scanner.scan(RE_INT))
222
- @tokens << [:token_int, value, @start]
223
- @start = @scanner.pos
224
- elsif (value = @scanner.scan(RE_PUNCTUATION))
225
- @tokens << [TOKEN_MAP[value] || :token_unknown, value, @start]
226
- @start = @scanner.pos
227
- elsif (value = @scanner.scan(RE_WORD))
228
- @tokens << [TOKEN_MAP[value] || :token_word, value, @start]
204
+ scan_string("'", :token_single_quote_string, RE_SINGLE_QUOTE_STRING_SPECIAL)
205
+ when "\""
229
206
  @start = @scanner.pos
207
+ scan_string("\"", :token_double_quote_string, RE_DOUBLE_QUOTE_STRING_SPECIAL)
230
208
  else
209
+ @scanner.pos -= 1
231
210
  break
232
211
  end
233
212
  end
@@ -413,9 +392,6 @@ module Liquid2
413
392
  end
414
393
 
415
394
  def lex_line_statements
416
- # TODO: For debugging. Comment this out when benchmarking.
417
- raise "must emit before accepting an expression token" if @scanner.pos != @start
418
-
419
395
  skip_trivia # Leading newlines are OK
420
396
 
421
397
  if (tag_name = @scanner.scan(/(?:[a-z][a-z_0-9]*|#)/))
@@ -231,7 +231,7 @@ module Liquid2
231
231
  def self.extract_filters(expression, template_name)
232
232
  filters = [] # : Array[[String, Span]]
233
233
 
234
- if expression.is_a?(Liquid2::FilteredExpression)
234
+ if expression.is_a?(Liquid2::FilteredExpression) && !expression.filters.nil?
235
235
  expression.filters.each do |filter|
236
236
  filters << [filter.name, Span.new(template_name, filter.token.last)]
237
237
  end
@@ -43,7 +43,6 @@ module Liquid2
43
43
  end
44
44
 
45
45
  def render_with_context(context, buffer, partial: false, block_scope: false, namespace: nil)
46
- # TODO: don't extend if namespace is nil
47
46
  context.extend(namespace || {}) do
48
47
  index = 0
49
48
  while (node = @ast[index])
@@ -59,6 +58,8 @@ module Liquid2
59
58
 
60
59
  next unless (interrupt = context.interrupts.pop)
61
60
 
61
+ break if interrupt == :stop_render
62
+
62
63
  if !partial || block_scope
63
64
  raise LiquidSyntaxError.new("unexpected #{interrupt}",
64
65
  node.token) # steep:ignore
@@ -69,15 +70,14 @@ module Liquid2
69
70
  end
70
71
  end
71
72
  rescue LiquidError => e
72
- e.source = @source
73
- e.template_name = @name unless @name.empty?
73
+ e.source = context.template.source unless e.source
74
+ e.template_name = @name unless e.template_name || @name.empty?
74
75
  raise
75
76
  end
76
77
 
77
78
  # Merge template globals with another namespace.
78
79
  def make_globals(namespace)
79
- # TODO: optimize
80
- @globals.merge(@overlay || {}, namespace || {})
80
+ @globals.merge(@overlay, namespace || {})
81
81
  end
82
82
 
83
83
  # Return `false` if this template is stale and needs to be loaded again.
@@ -123,6 +123,53 @@ module Liquid2
123
123
  nodes
124
124
  end
125
125
 
126
+ # Return an array of `{% doc %}` nodes found in this template.
127
+ #
128
+ # Each instance of `Liquid2::DocTag` has a `token` and `text` attribute. Use
129
+ # `Template#docs.map(&:text)` to get an array of doc strings.
130
+ #
131
+ # @return [Array[DocTag]]
132
+ def docs
133
+ context = RenderContext.new(self)
134
+ nodes = [] # : Array[DocTag]
135
+
136
+ # @type var visit: ^(Node) -> void
137
+ visit = lambda do |node|
138
+ nodes << node if node.is_a?(DocTag)
139
+
140
+ node.children(context, include_partials: false).each do |child|
141
+ visit.call(child) if child.is_a?(Node)
142
+ end
143
+ end
144
+
145
+ @ast.each { |node| visit.call(node) if node.is_a?(Node) }
146
+
147
+ nodes
148
+ end
149
+
150
+ # Return arrays of `{% macro %}` and `{% call %}` tags found in this template.
151
+ # @param include_partials [bool]
152
+ # @return [Array[MacroTag], Array[CallTag]]
153
+ def macros(include_partials: false)
154
+ context = RenderContext.new(self)
155
+ macro_nodes = [] # : Array[MacroTag]
156
+ call_nodes = [] # : Array[CallTag]
157
+
158
+ # @type var visit: ^(Node) -> void
159
+ visit = lambda do |node|
160
+ macro_nodes << node if node.is_a?(MacroTag)
161
+ call_nodes << node if node.is_a?(CallTag)
162
+
163
+ node.children(context, include_partials: include_partials).each do |child|
164
+ visit.call(child) if child.is_a?(Node)
165
+ end
166
+ end
167
+
168
+ @ast.each { |node| visit.call(node) if node.is_a?(Node) }
169
+
170
+ [macro_nodes, call_nodes]
171
+ end
172
+
126
173
  # Return an array of variables used in this template, without path segments.
127
174
  # @param include_partials [bool]
128
175
  # @return [Array[String]]