antlers 0.1.0 → 0.3.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7aef5ead7628092869dbae7f4ae7fee7bea88a69e1c3ee2e9ebd5fd6ace600ea
4
- data.tar.gz: a96ba7818b542305105305c9c7cbc73a6c4962b118e6f0686f1eb73cfda59a56
3
+ metadata.gz: 57d224246ca8ba02aac3045b0f06904d8cebb0cfdb7f3d518d3c646324a1e3b6
4
+ data.tar.gz: e8b7787ccfc284dbb1d370292ba4238576e346c75bc957d5da70067220635ba7
5
5
  SHA512:
6
- metadata.gz: ff1db9dd204501ae40f3ccc43ebf56b236aa1776ef233edd7a982d34ca5ad5d5486aaad29de46f6f382adc092cb54d5169a6a84443faae5799eb26cb7a9c96ee
7
- data.tar.gz: 5637d1f3cb287c70b7f47d0cb7405c594b98fc3858a624e10302ef935e88872f07fa5a67403bbb091a7ec29af758161f8effa9c2055ddd3a12ec2aa32d1d470a
6
+ metadata.gz: b4cb6632bbbafb3ec12cc27256b2ae9e2134e3b621e28a7edcfd269bfff828d128cd0f659d7135a734b7792d121477c0ca012c3215a1eef442d3106bb637e89d
7
+ data.tar.gz: 0a0f5f6fc83e72a7253a737956c3a4cff5961999722b23b96ad44716b3ad2b2e40ce7742145cea8610f9604467a438a12000a0609cd9408487f85472f362cb3d
data/lib/antlers.rb CHANGED
@@ -1,15 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'lexer'
4
+ require_relative 'parser'
5
+
3
6
  module Antlers
4
7
  class << self
5
- def parse(template)
8
+ def ast(template)
6
9
  return template unless template.include?('<{') || template.include?('{')
7
10
 
8
11
  lexemes = Lexer.new.parse(template)
9
12
  Parser.parse(lexemes)
10
13
  end
11
14
 
12
- # TODO: Render the AST.
13
- def render(antler_node); end
15
+ def render(ast:, current_binding:, parent_binding: nil, slot_node: nil, namespace: nil)
16
+ ast.render(current_binding:, parent_binding:, slot_node:, namespace:)
17
+ end
14
18
  end
15
19
  end
@@ -1,14 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative '../nodes/for_node'
3
4
  require_relative '../nodes/prop_node'
4
5
  require_relative '../nodes/slot_node'
5
6
  require_relative '../nodes/var_node'
7
+ require_relative '../nodes/yield_node'
6
8
 
7
9
  module Antlers
8
10
  class NodeFactory
9
11
  class << self
10
- def var_node(segment:)
11
- VarNode.new(name: segment[:ivar])
12
+ def for_node(segment:)
13
+ ForNode.new(name: segment[:for_def], item: segment[:for_def], items: segment[:in])
12
14
  end
13
15
 
14
16
  def prop_node(segment:)
@@ -18,6 +20,14 @@ module Antlers
18
20
  def slot_node(segment:)
19
21
  SlotNode.new(name: segment[:slot_def], props: segment[:props])
20
22
  end
23
+
24
+ def var_node(segment:)
25
+ VarNode.new(value: segment[:var])
26
+ end
27
+
28
+ def yield_node(segment:)
29
+ YieldNode.new(name: segment[:slot])
30
+ end
21
31
  end
22
32
  end
23
33
  end
@@ -8,7 +8,11 @@ module Antlers
8
8
  @name = name
9
9
  end
10
10
 
11
- # Consider this a value object on comparison.
11
+ def render(current_binding: nil, parent_binding: nil, slot_node: nil, namespace: nil)
12
+ raise NotImplementedError
13
+ end
14
+
15
+ # Consider instance a value object on comparison.
12
16
  def ==(other) = other.class == self.class
13
17
  def eql?(other) = self == other
14
18
  def hash = [self.class].hash
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'antler_node'
4
+
5
+ module Antlers
6
+ class BranchNode < AntlerNode
7
+ attr_accessor :children
8
+
9
+ def initialize(name:, children: [])
10
+ super(name:)
11
+
12
+ @children = children
13
+ end
14
+
15
+ def render(current_binding: nil, parent_binding: nil, slot_node: nil, namespace: nil)
16
+ output = ''
17
+
18
+ @children.each do |child|
19
+ # Antlers nodes respond to "render", whereas HTML is stored as a string and output as is.
20
+ output += (child.respond_to?(:render) ? child.render(current_binding:, parent_binding:, slot_node:, namespace:) : child) || ''
21
+ end
22
+
23
+ output
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'antler_node'
4
+
5
+ module Antlers
6
+ class LeafNode < AntlerNode
7
+ end
8
+ end
data/lib/lexer.rb CHANGED
@@ -1,12 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'queries'
4
+
3
5
  module Antlers
6
+ extend Queries
7
+
4
8
  class LexerParseError < StandardError; end
5
9
 
6
10
  class Lexer
7
11
  def initialize
8
12
  @delimiters = ['<{', '}>', '{', '}']
9
- @keywords = ['if:', 'for:', 'in:']
13
+ @keywords = ['if:', 'for:', 'in:', ':for', 'slot:', ':slot']
10
14
  @cursor = 0
11
15
  end
12
16
 
@@ -20,7 +24,7 @@ module Antlers
20
24
  until segments[@cursor].nil?
21
25
  if (antlers_segment = antlers_segment(segments:))
22
26
  sequence << antlers_lexeme(antlers_segment:, segments:)
23
- # Skipping: ['{', '@ivar', '}']
27
+ # Skipping: ['{', 'expression', '}']
24
28
  # Skipping: ['<{', 'name + props + keywords', '}>']
25
29
  @cursor += 3
26
30
  else
@@ -37,44 +41,72 @@ module Antlers
37
41
 
38
42
  def antlers_segment(segments:)
39
43
  next_segment = segments[@cursor + 1]
40
- return nil unless next_segment && (segments[@cursor] == '<{' || ivar?(segments:))
44
+ return nil unless next_segment && (segments[@cursor] == '<{' || var?(segments:))
41
45
 
42
46
  next_segment
43
47
  end
44
48
 
45
49
  def antlers_lexeme(antlers_segment:, segments:)
46
- return ivar(antlers_segment:) if ivar?(segments:)
50
+ return var(antlers_segment:) if var?(segments:)
47
51
 
48
- name, props, _keywords = parse_segment(antlers_segment:)
52
+ name, props, keywords = parse_segment(antlers_segment:)
49
53
 
54
+ return slot_yield if slot_yield?(keywords)
50
55
  return slot(name:, props:) if slot?(name)
51
56
  return prop(name:, props:) if prop?(name)
57
+ return for_loop(keywords:) if for_loop?(keywords:)
52
58
 
53
- raise LexerParseError, "Couldn't parse antlers syntax: '#{antlers_segment}'"
59
+ raise LexerParseError, "Unrecognised syntax: '#{antlers_segment}'"
54
60
  end
55
61
 
56
62
  def parse_segment(antlers_segment:)
57
63
  name_and_props, *keywords = antlers_segment.split(/(#{Regexp.union(@keywords)})/)
58
64
  name, *props = name_and_props.split(' ')
59
65
 
60
- [name, props, keywords]
66
+ [name, props, keywords.map(&:strip)]
67
+ end
68
+
69
+ def var?(segments:)
70
+ first, middle, last = segments[@cursor..@cursor + 3].map(&:strip)
71
+ first == '{' && last == '}'
61
72
  end
62
73
 
63
- def ivar?(segments:)
64
- first, second, third = segments[@cursor..@cursor + 3].map(&:strip)
65
- first == '{' && second&.start_with?('@') && third == '}'
74
+ def for_loop?(keywords:)
75
+ keywords.first == 'for:' || keywords.first == ':for'
66
76
  end
67
77
 
68
78
  def slot?(name)
69
- name.start_with?(':') || name.end_with?(':')
79
+ name && (name.start_with?(':') || name.end_with?(':'))
80
+ end
81
+
82
+ def slot_yield?(keywords)
83
+ keywords.include?(':slot')
70
84
  end
71
85
 
72
86
  def prop?(name)
73
- [*'A'..'Z'].include?(name[0])
87
+ name && [*'A'..'Z'].include?(name[0])
74
88
  end
75
89
 
76
- def ivar(antlers_segment:)
77
- { ivar: antlers_segment.delete_prefix('@') }
90
+ def var(antlers_segment:)
91
+ # String is already interpolated or not depending on user input on the template layer, now we store it without those template quotes.
92
+ if Queries.user_defined_string?(antlers_segment)
93
+ antlers_segment = antlers_segment[1..-2]
94
+ end
95
+
96
+ { var: antlers_segment }
97
+ end
98
+
99
+ def for_loop(keywords:)
100
+ key_values = keywords.count % 2 == 0 ? keywords.each_slice(2).to_h : {}
101
+
102
+ if key_values['for:']
103
+ for_def = { for_def: key_values['for:'] }
104
+ for_def[:in] = key_values['in:']
105
+ return for_def
106
+ end
107
+
108
+ # TODO: Keep track of which for loop we're in to allow nested for loops.
109
+ { for_end: 'level_1' }
78
110
  end
79
111
 
80
112
  def slot(name:, props:)
@@ -87,6 +119,10 @@ module Antlers
87
119
  { slot_end: name.delete_prefix(':') }
88
120
  end
89
121
 
122
+ def slot_yield
123
+ { slot: :default }
124
+ end
125
+
90
126
  def prop(name:, props:)
91
127
  prop = { prop: name }
92
128
  prop[:props] = props(props) unless props.empty?
@@ -108,7 +144,7 @@ module Antlers
108
144
  value = odd_props.shift
109
145
  end
110
146
 
111
- props[prop] = value
147
+ props[prop.to_sym] = value
112
148
  end
113
149
 
114
150
  props
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'low_event'
4
+
5
+ module Antlers
6
+ module Props
7
+ attr_accessor :props
8
+
9
+ def initialize(name:, props: {}, **)
10
+ super(name:, **)
11
+
12
+ @props = props
13
+ end
14
+
15
+ private
16
+
17
+ def create_render_event(props:)
18
+ Low::Events::RenderEvent.new(action: :render, props:)
19
+ end
20
+
21
+ def evaluate_props(props:, current_binding:)
22
+ return {} if props.nil?
23
+
24
+ evaluated_props = {}
25
+
26
+ props.each do |name, value|
27
+ receiver = current_binding.receiver
28
+
29
+ if receiver.respond_to?(value.to_sym)
30
+ evaluated_props[name] = receiver.send(value.to_sym)
31
+ elsif value.start_with?('@')
32
+ evaluated_props[name] = receiver.instance_variable_get(value)
33
+ end
34
+ end
35
+
36
+ evaluated_props
37
+ end
38
+
39
+ def class_from_namespace(namespace:, name:)
40
+ return Object.const_get(name) if Object.const_defined?(name) || name.start_with?('::') || namespace.empty?
41
+
42
+ namespace_with_name = [namespace, name].join('::')
43
+ return Object.const_get(namespace_with_name) if Object.const_defined?(namespace_with_name)
44
+
45
+ namespace.pop
46
+ class_from_namespace(namespace:, name:)
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,19 @@
1
+ module Antlers
2
+ module Variables
3
+ # A variable is deliberately limited in what it can represent.
4
+ # 1. An instance variable
5
+ # 2. A method call/local variable
6
+ # 3. A static string
7
+ def evaluate_variable(name:, current_binding:)
8
+ if current_binding
9
+ return current_binding.receiver.instance_variable_get(name) if name.start_with?('@')
10
+ return current_binding.local_variable_get(name) if current_binding.local_variable_defined?(name)
11
+ return current_binding.receiver.send(name.to_sym) if current_binding.receiver.respond_to?(name.to_sym)
12
+ end
13
+
14
+ @value.to_s
15
+ rescue NameError
16
+ @value.to_s
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../interfaces/branch_node'
4
+ require_relative '../modules/props'
5
+ require_relative '../modules/variables'
6
+
7
+ module Antlers
8
+ class ForNode < BranchNode
9
+ include Props
10
+ include Variables
11
+
12
+ attr_accessor :children
13
+
14
+ def initialize(name:, item:, items:, props: [], children: [])
15
+ super(name:, props:, children:)
16
+
17
+ @item = item
18
+ @items = items
19
+ end
20
+
21
+ def render(current_binding: nil, parent_binding: nil, slot_node: nil, namespace: nil)
22
+ output = ''
23
+
24
+ evaluate_variable(name: @items, current_binding:).each do |item|
25
+ current_binding.local_variable_set(@item, item)
26
+
27
+ @children.each do |child|
28
+ # Antlers nodes respond to "render", whereas HTML is stored as a string and output as is.
29
+ output += (child.respond_to?(:render) ? child.render(current_binding:, parent_binding:, slot_node:, namespace:) : child) || ''
30
+ end
31
+ end
32
+
33
+ output
34
+ end
35
+ end
36
+ end
@@ -1,15 +1,23 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative '../interfaces/antler_node'
3
+ require_relative '../interfaces/leaf_node'
4
+ require_relative '../modules/props'
4
5
 
5
6
  module Antlers
6
- attr_accessor :props
7
+ class PropNode < LeafNode
8
+ include Props
7
9
 
8
- class PropNode < AntlerNode
9
- def initialize(name:, props: [])
10
- super(name:)
10
+ def render(current_binding: nil, parent_binding: nil, slot_node: nil, namespace: nil)
11
+ props = evaluate_props(props: @props, current_binding:)
12
+ event = create_render_event(props:)
11
13
 
12
- @props = props
14
+ renderable_klass = class_from_namespace(namespace: namespace&.split('::') || [], name: @name)
15
+ renderable_instance = renderable_klass.new(event:)
16
+
17
+ # Classes referenced via "<{ ChildNode }>" must implement class/instance render/render_template methods (See LowNode).
18
+ return renderable_instance.render_template(event:, parent_binding:, props:) if renderable_klass.template
19
+
20
+ props.empty? ? renderable_instance.render(event:) : renderable_instance.render(event:, **props)
13
21
  end
14
22
  end
15
23
  end
@@ -1,15 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative '../interfaces/antler_node'
3
+ require_relative '../interfaces/branch_node'
4
4
 
5
5
  module Antlers
6
- class RootNode < AntlerNode
7
- attr_accessor :children
8
-
6
+ class RootNode < BranchNode
9
7
  def initialize(name: :root_node, children: [])
10
- super(name:)
11
-
12
- @children = children
8
+ super(name:, children:)
13
9
  end
14
10
  end
15
11
  end
@@ -1,15 +1,29 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative '../nodes/prop_node'
3
+ require_relative '../interfaces/branch_node'
4
+ require_relative '../modules/props'
4
5
 
5
6
  module Antlers
6
- class SlotNode < PropNode
7
+ class SlotNode < BranchNode
8
+ include Props
9
+
7
10
  attr_accessor :children
8
11
 
9
12
  def initialize(name:, props: [], children: [])
10
- super(name:, props:)
13
+ super(name:, props:, children:)
14
+ end
15
+
16
+ def render(current_binding: nil, parent_binding: nil, slot_node: nil, namespace: nil)
17
+ props = evaluate_props(props: @props, current_binding:)
18
+ event = create_render_event(props:)
19
+
20
+ klass = class_from_namespace(namespace: namespace&.split('::') || [], name: @name)
21
+ instance = klass.new(event:)
22
+
23
+ # Classes referenced via "<{ ChildNode }>" must implement class/instance render/render_template methods (See LowNode).
24
+ return instance.render_template(event:, parent_binding: current_binding, slot_node: self, props:) if klass.template
11
25
 
12
- @children = children
26
+ props.empty? ? instance.render(event:) : instance.render(event:, **props)
13
27
  end
14
28
  end
15
29
  end
@@ -1,8 +1,24 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative '../interfaces/antler_node'
3
+ require 'erb'
4
+
5
+ require_relative '../interfaces/leaf_node'
6
+ require_relative '../modules/variables'
4
7
 
5
8
  module Antlers
6
- class VarNode < AntlerNode
9
+ class VarNode < LeafNode
10
+ include Variables
11
+
12
+ attr_reader :value
13
+
14
+ def initialize(name: :var, value:)
15
+ super(name:)
16
+
17
+ @value = value
18
+ end
19
+
20
+ def render(current_binding: nil, parent_binding: nil, slot_node: nil, namespace: nil)
21
+ ERB::Util.html_escape(evaluate_variable(name: @value, current_binding:) || @value)
22
+ end
7
23
  end
8
24
  end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../interfaces/leaf_node'
4
+
5
+ module Antlers
6
+ class YieldNode < BranchNode
7
+ def initialize(name: :default)
8
+ super(name:)
9
+ end
10
+
11
+ # Renders the children of the parent node in the binding of the parent.
12
+ def render(current_binding: nil, parent_binding: nil, slot_node: nil, namespace: nil)
13
+ output = ''
14
+
15
+ slot_node.children.each do |child|
16
+ # Antlers nodes respond to "render", whereas HTML is stored as a string and output as is.
17
+ output += (child.respond_to?(:render) ? child.render(current_binding: parent_binding, parent_binding: nil, slot_node: nil, namespace:) : child) || ''
18
+ end
19
+
20
+ output
21
+ end
22
+ end
23
+ end
data/lib/parser.rb CHANGED
@@ -7,31 +7,40 @@ module Antlers
7
7
  module Parser
8
8
  class << self
9
9
  def parse(sequence, id: :root_node)
10
- branch(branch_node: RootNode.new(name: id), sequence:)
10
+ branch(node: RootNode.new(name: id), sequence:)
11
11
  end
12
12
 
13
- def branch(branch_node:, sequence:) # rubocop:disable Metrics/AbcSize
13
+ def branch(node:, sequence:) # rubocop:disable Metrics/AbcSize
14
14
  until sequence.empty?
15
15
  segment = sequence.shift
16
16
 
17
17
  if segment.is_a?(String)
18
- branch_node.children << segment
19
- elsif segment[:ivar]
20
- branch_node.children << NodeFactory.var_node(segment:)
18
+ node.children << segment
19
+ elsif segment[:var]
20
+ node.children << NodeFactory.var_node(segment:)
21
21
  elsif segment[:prop]
22
- branch_node.children << NodeFactory.prop_node(segment:)
22
+ node.children << NodeFactory.prop_node(segment:)
23
+ elsif segment[:slot]
24
+ node.children << NodeFactory.yield_node(segment:)
23
25
  elsif segment[:slot_def]
24
26
  slot_node = NodeFactory.slot_node(segment:)
25
- branch_node.children << slot_node
26
-
27
- sub_sequence = []
28
- sub_sequence << sequence.shift until sequence.first[:slot_end] == slot_node.name
29
-
30
- branch(branch_node: slot_node, sequence: sub_sequence)
27
+ node.children << slot_node
28
+ sub_branch(node: slot_node, sequence:, end_key: :slot_end, end_name: slot_node.name)
29
+ elsif segment[:for_def]
30
+ for_node = NodeFactory.for_node(segment:)
31
+ node.children << for_node
32
+ sub_branch(node: for_node, sequence:, end_key: :for_end, end_name: 'level_1')
31
33
  end
32
34
  end
33
35
 
34
- branch_node
36
+ node
37
+ end
38
+
39
+ def sub_branch(node:, sequence:, end_key:, end_name: nil)
40
+ sub_sequence = []
41
+ sub_sequence << sequence.shift until sequence.first.is_a?(Hash) && sequence.first[end_key] == end_name
42
+
43
+ branch(node:, sequence: sub_sequence)
35
44
  end
36
45
  end
37
46
  end
data/lib/queries.rb ADDED
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Antlers
4
+ module Queries
5
+ class << self
6
+ def user_defined_string?(string)
7
+ wrapped_in?(string, %q{'}) || wrapped_in?(string, %q{"})
8
+ end
9
+
10
+ def wrapped_in?(string, delimeter)
11
+ string[0] == delimeter && string[-1] == delimeter
12
+ end
13
+ end
14
+ end
15
+ end
data/lib/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Antlers
4
- VERSION = '0.1.0'
4
+ VERSION = '0.3.0'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: antlers
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - maedi
@@ -10,19 +10,33 @@ cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
- name: low_type
13
+ name: low_event
14
14
  requirement: !ruby/object:Gem::Requirement
15
15
  requirements:
16
- - - "~>"
16
+ - - ">="
17
17
  - !ruby/object:Gem::Version
18
- version: '1.0'
18
+ version: '0'
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
- - - "~>"
23
+ - - ">="
24
24
  - !ruby/object:Gem::Version
25
- version: '1.0'
25
+ version: '0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: erb
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
26
40
  description: A new Ruby templating language. Coming soon...
27
41
  email:
28
42
  - maediprichard@gmail.com
@@ -33,12 +47,19 @@ files:
33
47
  - lib/antlers.rb
34
48
  - lib/factories/node_factory.rb
35
49
  - lib/interfaces/antler_node.rb
50
+ - lib/interfaces/branch_node.rb
51
+ - lib/interfaces/leaf_node.rb
36
52
  - lib/lexer.rb
53
+ - lib/modules/props.rb
54
+ - lib/modules/variables.rb
55
+ - lib/nodes/for_node.rb
37
56
  - lib/nodes/prop_node.rb
38
57
  - lib/nodes/root_node.rb
39
58
  - lib/nodes/slot_node.rb
40
59
  - lib/nodes/var_node.rb
60
+ - lib/nodes/yield_node.rb
41
61
  - lib/parser.rb
62
+ - lib/queries.rb
42
63
  - lib/version.rb
43
64
  homepage: https://codeberg.org/raindeer/antlers
44
65
  licenses: []
@@ -59,7 +80,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
59
80
  - !ruby/object:Gem::Version
60
81
  version: '0'
61
82
  requirements: []
62
- rubygems_version: 3.7.2
83
+ rubygems_version: 4.0.10
63
84
  specification_version: 4
64
85
  summary: Deer to be different
65
86
  test_files: []