antlers 0.8.1 → 0.9.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 +4 -4
- data/lib/antlers/elements.rb +137 -0
- data/lib/antlers/lexer.rb +97 -0
- data/lib/antlers/parser.rb +46 -0
- data/lib/{version.rb → antlers/version.rb} +1 -1
- data/lib/antlers.rb +12 -7
- data/lib/interfaces/antler_node.rb +18 -1
- data/lib/interfaces/branch_node.rb +1 -2
- data/lib/interfaces/lexeme.rb +24 -1
- data/lib/lexemes/prop_lexeme.rb +0 -23
- data/lib/lexemes/var_lexeme.rb +26 -0
- data/lib/modules/variables.rb +1 -1
- data/lib/nodes/for_node.rb +15 -2
- data/lib/nodes/form_node.rb +17 -2
- data/lib/nodes/html_node.rb +27 -0
- data/lib/nodes/if_node.rb +14 -2
- data/lib/nodes/prop_node.rb +10 -0
- data/lib/nodes/root_node.rb +10 -0
- data/lib/nodes/slot_node.rb +19 -2
- data/lib/nodes/var_node.rb +10 -0
- data/lib/nodes/yield_node.rb +13 -8
- metadata +21 -5
- data/lib/factories/node_factory.rb +0 -57
- data/lib/lexer.rb +0 -120
- data/lib/parser.rb +0 -59
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f16d22b587d7dbdc07a0a51fc19032271be9347e90eb09c77541a56099f8088f
|
|
4
|
+
data.tar.gz: b1437b4cac8a748fc23fc6eaf06c07e5d3111d63d5100651afb9195f488dacfb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ecc5de204a45f9cda60f03aeefc49aee735fc9aa3da5fd20b1d12959497bfb4683ab299bd64d4ae8795b3296ab9e018c8b8eb70672f3788aeeeaf8355f03d07e
|
|
7
|
+
data.tar.gz: 0b14edede169971914c9958f4b3369c361271566d36c883dd3c1774ef968e8299d2d95c58a90e921ea2bbae5d70f4a2634c8ce00217e058c2ee6c71a9ec3a42f
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'plugs'
|
|
4
|
+
|
|
5
|
+
module Antlers
|
|
6
|
+
class Elements
|
|
7
|
+
include Plugs
|
|
8
|
+
|
|
9
|
+
plug(:root) do
|
|
10
|
+
plug(:node) do
|
|
11
|
+
require_relative '../nodes/root_node'
|
|
12
|
+
RootNode
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
plug(:html) do
|
|
17
|
+
plug(:node) do
|
|
18
|
+
lazy do
|
|
19
|
+
require_relative '../nodes/html_node'
|
|
20
|
+
HTMLNode
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
plug(:form) do
|
|
26
|
+
plug(:lexeme) do
|
|
27
|
+
lazy do
|
|
28
|
+
require_relative '../lexemes/form_lexeme'
|
|
29
|
+
FormLexeme
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
plug(:node) do
|
|
34
|
+
lazy do
|
|
35
|
+
require_relative '../nodes/form_node'
|
|
36
|
+
FormNode
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
plug(:for) do
|
|
42
|
+
plug(:lexeme) do
|
|
43
|
+
lazy do
|
|
44
|
+
require_relative '../lexemes/for_lexeme'
|
|
45
|
+
ForLexeme
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
plug(:node) do
|
|
50
|
+
lazy do
|
|
51
|
+
require_relative '../nodes/for_node'
|
|
52
|
+
ForNode
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
plug(:if) do
|
|
58
|
+
plug(:lexeme) do
|
|
59
|
+
lazy do
|
|
60
|
+
require_relative '../lexemes/if_lexeme'
|
|
61
|
+
IfLexeme
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
plug(:node) do
|
|
66
|
+
lazy do
|
|
67
|
+
require_relative '../nodes/if_node'
|
|
68
|
+
IfNode
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
plug(:prop) do
|
|
74
|
+
plug(:lexeme) do
|
|
75
|
+
lazy do
|
|
76
|
+
require_relative '../lexemes/prop_lexeme'
|
|
77
|
+
PropLexeme
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
plug(:node) do
|
|
82
|
+
lazy do
|
|
83
|
+
require_relative '../nodes/prop_node'
|
|
84
|
+
PropNode
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
plug(:slot) do
|
|
90
|
+
plug(:lexeme) do
|
|
91
|
+
lazy do
|
|
92
|
+
require_relative '../lexemes/slot_lexeme'
|
|
93
|
+
SlotLexeme
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
plug(:node) do
|
|
98
|
+
lazy do
|
|
99
|
+
require_relative '../nodes/slot_node'
|
|
100
|
+
SlotNode
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
plug(:yield) do
|
|
106
|
+
plug(:lexeme) do
|
|
107
|
+
lazy do
|
|
108
|
+
require_relative '../lexemes/yield_lexeme'
|
|
109
|
+
YieldLexeme
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
plug(:node) do
|
|
114
|
+
lazy do
|
|
115
|
+
require_relative '../nodes/yield_node'
|
|
116
|
+
YieldNode
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
plug(:var) do
|
|
122
|
+
plug(:node) do
|
|
123
|
+
lazy do
|
|
124
|
+
require_relative '../nodes/var_node'
|
|
125
|
+
VarNode
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
plug(:lexeme) do
|
|
130
|
+
lazy do
|
|
131
|
+
require_relative '../lexemes/var_lexeme'
|
|
132
|
+
VarLexeme
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../lexemes/var_lexeme'
|
|
4
|
+
|
|
5
|
+
module Antlers
|
|
6
|
+
class LexerError < StandardError; end
|
|
7
|
+
|
|
8
|
+
class Lexer
|
|
9
|
+
def initialize(lexeme_types:)
|
|
10
|
+
@lexeme_types = lexeme_types.to_a
|
|
11
|
+
@var_lexeme = @lexeme_types.find { it == VarLexeme }
|
|
12
|
+
|
|
13
|
+
@delimiters = ['<{', '}>']
|
|
14
|
+
@delimiters = [*@delimiters, '{', '}'] if @var_lexeme
|
|
15
|
+
|
|
16
|
+
@keywords = @lexeme_types.flat_map { |lexeme| lexeme.const_get(:KEYWORDS) }
|
|
17
|
+
@cursor = 0
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def parse(template:)
|
|
21
|
+
@cursor = 0
|
|
22
|
+
sequence = []
|
|
23
|
+
|
|
24
|
+
# Split on delimiters and retain capture groups.
|
|
25
|
+
segments = template.split(/(#{Regexp.union(@delimiters)})/).map(&:strip)
|
|
26
|
+
|
|
27
|
+
until segments[@cursor].nil?
|
|
28
|
+
if (segment = antlers_segment(segments:))
|
|
29
|
+
sequence << antlers_lexeme(segment:, segments:)
|
|
30
|
+
# Skipping: ['{', 'expression', '}']
|
|
31
|
+
# Skipping: ['<{', 'name + props + keywords', '}>']
|
|
32
|
+
@cursor += 3
|
|
33
|
+
else
|
|
34
|
+
segment = segments[@cursor]
|
|
35
|
+
sequence << segment unless segment.empty?
|
|
36
|
+
@cursor += 1
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
sequence
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def antlers_segment(segments:)
|
|
46
|
+
next_segment = segments[@cursor + 1]
|
|
47
|
+
return nil unless next_segment && (segments[@cursor] == '<{' || brackets?(segments:))
|
|
48
|
+
|
|
49
|
+
next_segment
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def antlers_lexeme(segment:, segments:)
|
|
53
|
+
name, props, keywords = parse_antlers_segment(segment:)
|
|
54
|
+
|
|
55
|
+
return @var_lexeme.lexeme(segment:, raw: false) if @var_lexeme && brackets?(segments:)
|
|
56
|
+
|
|
57
|
+
lexeme = @lexeme_types.find { it.match?(name:, keywords:) }
|
|
58
|
+
return lexeme.lexeme(name:, props:, keywords:) if lexeme
|
|
59
|
+
|
|
60
|
+
return @var_lexeme.lexeme(segment:, raw: true) if @var_lexeme && deerheads?(segments:)
|
|
61
|
+
|
|
62
|
+
raise LexerError, "Unrecognised syntax: '#{segment}'"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def parse_antlers_segment(segment:)
|
|
66
|
+
name_and_props, *keywords = segment.split(/(#{Regexp.union(@keywords)})/)
|
|
67
|
+
name, *props = name_and_props.split(' ')
|
|
68
|
+
[name, props, parse_keywords(keywords:)]
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def parse_keywords(keywords:)
|
|
72
|
+
key_values = {}
|
|
73
|
+
|
|
74
|
+
while (keyword = keywords.shift)
|
|
75
|
+
keyword.strip!
|
|
76
|
+
value = keyword.end_with?(':') && value?(keywords.first.strip) ? keywords.shift.strip : nil
|
|
77
|
+
key_values[keyword] = value
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
key_values
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def value?(string)
|
|
84
|
+
!(string.start_with?(':') || string.end_with?(':'))
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def brackets?(segments:)
|
|
88
|
+
first, _, last = segments[@cursor..@cursor + 3].map(&:strip)
|
|
89
|
+
first == '{' && last == '}'
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def deerheads?(segments:)
|
|
93
|
+
first, _, last = segments[@cursor..@cursor + 3].map(&:strip)
|
|
94
|
+
first == '<{' && last == '}>'
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Antlers
|
|
4
|
+
class Parser
|
|
5
|
+
class ParserError < StandardError; end
|
|
6
|
+
|
|
7
|
+
def initialize(node_types:, namespace: nil)
|
|
8
|
+
@node_types = node_types.to_a
|
|
9
|
+
@namespace = namespace
|
|
10
|
+
@template = nil
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def parse(sequence:, template: nil, id: :root_node)
|
|
14
|
+
@template = template
|
|
15
|
+
|
|
16
|
+
branch(node: RootNode.new(name: id), sequence:)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def branch(node:, sequence:)
|
|
20
|
+
until sequence.empty?
|
|
21
|
+
segment = sequence.shift
|
|
22
|
+
|
|
23
|
+
child_class = @node_types.find { |n| n.match?(segment:) }
|
|
24
|
+
raise(ParserError, "No node matches #{segment}") unless child_class
|
|
25
|
+
|
|
26
|
+
child = child_class.build(segment:, namespace: @namespace, template: @template)
|
|
27
|
+
node.children << child
|
|
28
|
+
|
|
29
|
+
if (end_key = child_class::END_KEY)
|
|
30
|
+
sub_branch(node: child, sequence:, end_key:, end_name: child.end_name)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
node
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def sub_branch(node:, sequence:, end_key:, end_name: nil)
|
|
38
|
+
sub_sequence = []
|
|
39
|
+
sub_sequence << sequence.shift until sequence.first.is_a?(Hash) && sequence.first[end_key] == end_name
|
|
40
|
+
# Remove the end tag we just stopped before.
|
|
41
|
+
sequence.shift
|
|
42
|
+
|
|
43
|
+
branch(node:, sequence: sub_sequence)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
data/lib/antlers.rb
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative '
|
|
4
|
-
require_relative '
|
|
3
|
+
require_relative 'antlers/elements'
|
|
4
|
+
require_relative 'antlers/lexer'
|
|
5
|
+
require_relative 'antlers/parser'
|
|
6
|
+
|
|
7
|
+
# These requires allow downstream libraries to build their own antlers nodes.
|
|
8
|
+
require_relative 'interfaces/branch_node'
|
|
9
|
+
require_relative 'interfaces/leaf_node'
|
|
5
10
|
|
|
6
11
|
module Antlers
|
|
7
|
-
|
|
8
|
-
def ast(template:, namespace: nil)
|
|
9
|
-
return template unless template.include?('<{') || template.include?('{')
|
|
12
|
+
DEFAULT_ELEMENTS = [:root, :html, :form, :for, :if, :prop, :slot, :yield, :var]
|
|
10
13
|
|
|
11
|
-
|
|
12
|
-
|
|
14
|
+
class << self
|
|
15
|
+
def ast(template:, namespace: nil, elements: Elements[*DEFAULT_ELEMENTS])
|
|
16
|
+
sequence = Lexer.new(lexeme_types: elements[:lexeme]).parse(template:)
|
|
17
|
+
Parser.new(namespace:, node_types: elements[:node]).parse(sequence:, template:)
|
|
13
18
|
end
|
|
14
19
|
|
|
15
20
|
def render(ast:, current_binding:, parent_binding: nil, slot_node: nil)
|
|
@@ -4,6 +4,9 @@ module Antlers
|
|
|
4
4
|
class AntlerNode
|
|
5
5
|
attr_reader :name
|
|
6
6
|
|
|
7
|
+
DEF_KEY = nil
|
|
8
|
+
END_KEY = nil
|
|
9
|
+
|
|
7
10
|
def initialize(name:)
|
|
8
11
|
@name = name
|
|
9
12
|
end
|
|
@@ -12,7 +15,21 @@ module Antlers
|
|
|
12
15
|
raise NotImplementedError
|
|
13
16
|
end
|
|
14
17
|
|
|
15
|
-
|
|
18
|
+
def end_name
|
|
19
|
+
'level_1'
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
class << self
|
|
23
|
+
def match?(segment:)
|
|
24
|
+
raise NotImplementedError
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def build(segment:, namespace:)
|
|
28
|
+
raise NotImplementedError
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Compare instance as a value object.
|
|
16
33
|
def ==(other) = other.class == self.class
|
|
17
34
|
def eql?(other) = self == other
|
|
18
35
|
def hash = [self.class].hash
|
|
@@ -16,8 +16,7 @@ module Antlers
|
|
|
16
16
|
output = ''
|
|
17
17
|
|
|
18
18
|
@children.each do |child|
|
|
19
|
-
|
|
20
|
-
output += (child.respond_to?(:render) ? child.render(current_binding:, parent_binding:, slot_node:) : child) || ''
|
|
19
|
+
output += child.render(current_binding:, parent_binding:, slot_node:) || ''
|
|
21
20
|
end
|
|
22
21
|
|
|
23
22
|
output
|
data/lib/interfaces/lexeme.rb
CHANGED
|
@@ -6,8 +6,31 @@ module Antlers
|
|
|
6
6
|
|
|
7
7
|
def match?(name: nil, keywords: {}) = false
|
|
8
8
|
|
|
9
|
-
def lexeme(name:, props:, keywords:)
|
|
9
|
+
def lexeme(name:, props:, keywords:, segment: nil, raw: false)
|
|
10
10
|
raise NotImplementedError
|
|
11
11
|
end
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
def props(props)
|
|
16
|
+
odd_props = props.join(' ').split(/(=)|\s/)
|
|
17
|
+
|
|
18
|
+
return {} unless odd_props.any?
|
|
19
|
+
|
|
20
|
+
props = {}
|
|
21
|
+
until odd_props.empty?
|
|
22
|
+
prop = odd_props.shift
|
|
23
|
+
value = nil
|
|
24
|
+
|
|
25
|
+
if odd_props.first == '='
|
|
26
|
+
odd_props.shift
|
|
27
|
+
value = odd_props.shift
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
props[prop.to_sym] = value
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
props
|
|
34
|
+
end
|
|
12
35
|
end
|
|
13
36
|
end
|
data/lib/lexemes/prop_lexeme.rb
CHANGED
|
@@ -16,28 +16,5 @@ module Antlers
|
|
|
16
16
|
prop[:props] = props(props) unless props.empty?
|
|
17
17
|
prop
|
|
18
18
|
end
|
|
19
|
-
|
|
20
|
-
private
|
|
21
|
-
|
|
22
|
-
def props(props)
|
|
23
|
-
odd_props = props.join(' ').split(/(=)|\s/)
|
|
24
|
-
|
|
25
|
-
return {} unless odd_props.any?
|
|
26
|
-
|
|
27
|
-
props = {}
|
|
28
|
-
until odd_props.empty?
|
|
29
|
-
prop = odd_props.shift
|
|
30
|
-
value = nil
|
|
31
|
-
|
|
32
|
-
if odd_props.first == '='
|
|
33
|
-
odd_props.shift
|
|
34
|
-
value = odd_props.shift
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
props[prop.to_sym] = value
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
props
|
|
41
|
-
end
|
|
42
19
|
end
|
|
43
20
|
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../interfaces/lexeme'
|
|
4
|
+
require_relative '../support/queries'
|
|
5
|
+
|
|
6
|
+
module Antlers
|
|
7
|
+
module VarLexeme
|
|
8
|
+
include Lexeme
|
|
9
|
+
extend Queries
|
|
10
|
+
extend self
|
|
11
|
+
|
|
12
|
+
# Variables are special and are matched by the lexer.
|
|
13
|
+
def match?(**)
|
|
14
|
+
false
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def lexeme(segment:, raw:, **)
|
|
18
|
+
# String is already interpolated or not depending on user input on the template layer, now we store it without those template quotes.
|
|
19
|
+
segment = segment[1..-2] if Queries.user_defined_string?(segment)
|
|
20
|
+
|
|
21
|
+
return { raw_var: segment } if raw
|
|
22
|
+
|
|
23
|
+
{ var: segment }
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
data/lib/modules/variables.rb
CHANGED
|
@@ -6,7 +6,7 @@ module Antlers
|
|
|
6
6
|
module Variables
|
|
7
7
|
extend Queries
|
|
8
8
|
|
|
9
|
-
# Evaluation limited to the following for security and to
|
|
9
|
+
# Evaluation limited to the following for security and to stop a templating language becoming a programming language.
|
|
10
10
|
# 1. An instance variable
|
|
11
11
|
# 2. A method call/local variable
|
|
12
12
|
# 3. A method chain
|
data/lib/nodes/for_node.rb
CHANGED
|
@@ -9,6 +9,9 @@ module Antlers
|
|
|
9
9
|
include Props
|
|
10
10
|
include Variables
|
|
11
11
|
|
|
12
|
+
DEF_KEY = :for_def
|
|
13
|
+
END_KEY = :for_end
|
|
14
|
+
|
|
12
15
|
def initialize(name:, items:, value:, key: nil, props: [], children: [])
|
|
13
16
|
super(name:, props:, children:)
|
|
14
17
|
|
|
@@ -28,12 +31,22 @@ module Antlers
|
|
|
28
31
|
current_binding.local_variable_set(@key, key) if @key
|
|
29
32
|
|
|
30
33
|
@children.each do |child|
|
|
31
|
-
|
|
32
|
-
output += (child.respond_to?(:render) ? child.render(current_binding:, parent_binding:, slot_node:) : child) || ''
|
|
34
|
+
output += child.render(current_binding:, parent_binding:, slot_node:) || ''
|
|
33
35
|
end
|
|
34
36
|
end
|
|
35
37
|
|
|
36
38
|
output
|
|
37
39
|
end
|
|
40
|
+
|
|
41
|
+
class << self
|
|
42
|
+
def match?(segment:)
|
|
43
|
+
segment[DEF_KEY]
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def build(segment:, **)
|
|
47
|
+
value, key, items = segment.values_at(DEF_KEY, :key, :in)
|
|
48
|
+
new(name: value, key:, value:, items:)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
38
51
|
end
|
|
39
52
|
end
|
data/lib/nodes/form_node.rb
CHANGED
|
@@ -9,6 +9,9 @@ module Antlers
|
|
|
9
9
|
include Props
|
|
10
10
|
include Variables
|
|
11
11
|
|
|
12
|
+
DEF_KEY = :form_def
|
|
13
|
+
END_KEY = :form_end
|
|
14
|
+
|
|
12
15
|
def initialize(name:, action: nil, method: 'POST', props: [], children: [])
|
|
13
16
|
super(name:, props:, children:)
|
|
14
17
|
|
|
@@ -20,12 +23,24 @@ module Antlers
|
|
|
20
23
|
output = "<form action='#{@action}' method='#{@method}'>"
|
|
21
24
|
|
|
22
25
|
@children.each do |child|
|
|
23
|
-
|
|
24
|
-
output += (child.respond_to?(:render) ? child.render(current_binding:, parent_binding:, slot_node:) : child) || ''
|
|
26
|
+
output += child.render(current_binding:, parent_binding:, slot_node:) || ''
|
|
25
27
|
end
|
|
26
28
|
|
|
27
29
|
output += '</form>'
|
|
28
30
|
output
|
|
29
31
|
end
|
|
32
|
+
|
|
33
|
+
class << self
|
|
34
|
+
def match?(segment:)
|
|
35
|
+
segment[DEF_KEY]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def build(segment:, **)
|
|
39
|
+
action, method = segment.values_at(DEF_KEY, :method)
|
|
40
|
+
return new(name: action, action:, method:) if method
|
|
41
|
+
|
|
42
|
+
new(name: action, action:)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
30
45
|
end
|
|
31
46
|
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../interfaces/leaf_node'
|
|
4
|
+
|
|
5
|
+
module Antlers
|
|
6
|
+
class HTMLNode < LeafNode
|
|
7
|
+
def initialize(html:)
|
|
8
|
+
super(name: nil)
|
|
9
|
+
|
|
10
|
+
@html = html
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def render(**)
|
|
14
|
+
@html
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
class << self
|
|
18
|
+
def match?(segment:)
|
|
19
|
+
segment.is_a?(String)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def build(segment:, **)
|
|
23
|
+
new(html: segment)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
data/lib/nodes/if_node.rb
CHANGED
|
@@ -9,6 +9,9 @@ module Antlers
|
|
|
9
9
|
include Props
|
|
10
10
|
include Variables
|
|
11
11
|
|
|
12
|
+
DEF_KEY = :if_def
|
|
13
|
+
END_KEY = :if_end
|
|
14
|
+
|
|
12
15
|
def initialize(name:, value:, props: [], children: [])
|
|
13
16
|
super(name:, props:, children:)
|
|
14
17
|
|
|
@@ -20,12 +23,21 @@ module Antlers
|
|
|
20
23
|
|
|
21
24
|
if evaluate(name: @value, current_binding:)
|
|
22
25
|
@children.each do |child|
|
|
23
|
-
|
|
24
|
-
output += (child.respond_to?(:render) ? child.render(current_binding:, parent_binding:, slot_node:) : child) || ''
|
|
26
|
+
output += child.render(current_binding:, parent_binding:, slot_node:) || ''
|
|
25
27
|
end
|
|
26
28
|
end
|
|
27
29
|
|
|
28
30
|
output
|
|
29
31
|
end
|
|
32
|
+
|
|
33
|
+
class << self
|
|
34
|
+
def match?(segment:)
|
|
35
|
+
segment[DEF_KEY]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def build(segment:, **)
|
|
39
|
+
new(name: segment[DEF_KEY], value: segment[DEF_KEY])
|
|
40
|
+
end
|
|
41
|
+
end
|
|
30
42
|
end
|
|
31
43
|
end
|
data/lib/nodes/prop_node.rb
CHANGED
|
@@ -30,6 +30,16 @@ module Antlers
|
|
|
30
30
|
render_args(class_proxy:, instance:, event:, props:)
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
+
class << self
|
|
34
|
+
def match?(segment:)
|
|
35
|
+
segment[:prop]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def build(segment:, namespace:, **)
|
|
39
|
+
new(name: segment[:prop], props: segment[:props], namespace:)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
33
43
|
private
|
|
34
44
|
|
|
35
45
|
def create_instance(class_proxy:, klass:, event:, props:)
|
data/lib/nodes/root_node.rb
CHANGED
data/lib/nodes/slot_node.rb
CHANGED
|
@@ -7,11 +7,14 @@ require_relative '../modules/props'
|
|
|
7
7
|
module Antlers
|
|
8
8
|
class SlotNode < BranchNode
|
|
9
9
|
include Namespace
|
|
10
|
-
include Props #
|
|
10
|
+
include Props # The immediate ancestor which props are passed to.
|
|
11
|
+
|
|
12
|
+
DEF_KEY = :slot_def
|
|
13
|
+
END_KEY = :slot_end
|
|
11
14
|
|
|
12
15
|
attr_accessor :children
|
|
13
16
|
|
|
14
|
-
def initialize(name:, namespace:, props:
|
|
17
|
+
def initialize(name:, namespace:, props: {}, children: [], **)
|
|
15
18
|
super(name:, props:, children:)
|
|
16
19
|
|
|
17
20
|
@namespace = namespace
|
|
@@ -30,5 +33,19 @@ module Antlers
|
|
|
30
33
|
|
|
31
34
|
props.empty? ? instance.render(event:) : instance.render(event:, **props)
|
|
32
35
|
end
|
|
36
|
+
|
|
37
|
+
def end_name
|
|
38
|
+
@name
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
class << self
|
|
42
|
+
def match?(segment:)
|
|
43
|
+
segment[DEF_KEY]
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def build(segment:, namespace:, **)
|
|
47
|
+
new(name: segment[DEF_KEY], props: segment[:props], namespace:)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
33
50
|
end
|
|
34
51
|
end
|
data/lib/nodes/var_node.rb
CHANGED
|
@@ -25,5 +25,15 @@ module Antlers
|
|
|
25
25
|
|
|
26
26
|
ERB::Util.html_escape(result)
|
|
27
27
|
end
|
|
28
|
+
|
|
29
|
+
class << self
|
|
30
|
+
def match?(segment:)
|
|
31
|
+
segment[:var] || segment[:raw_var]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def build(segment:, **)
|
|
35
|
+
new(value: segment[:var] || segment[:raw_var], raw: !!segment[:raw_var])
|
|
36
|
+
end
|
|
37
|
+
end
|
|
28
38
|
end
|
|
29
39
|
end
|
data/lib/nodes/yield_node.rb
CHANGED
|
@@ -8,20 +8,25 @@ module Antlers
|
|
|
8
8
|
super(name:)
|
|
9
9
|
end
|
|
10
10
|
|
|
11
|
-
#
|
|
12
|
-
def render(current_binding: nil, parent_binding
|
|
11
|
+
# Render the children in the binding of the parent.
|
|
12
|
+
def render(current_binding: nil, parent_binding:, slot_node:)
|
|
13
13
|
output = ''
|
|
14
14
|
|
|
15
15
|
slot_node.children.each do |child|
|
|
16
|
-
|
|
17
|
-
if child.respond_to?(:render) # rubocop:disable Style/ConditionalAssignment
|
|
18
|
-
output += child.render(current_binding: parent_binding, parent_binding: nil, slot_node: nil)
|
|
19
|
-
else
|
|
20
|
-
output += child || ''
|
|
21
|
-
end
|
|
16
|
+
output += child.render(current_binding: parent_binding, parent_binding:, slot_node:) || ''
|
|
22
17
|
end
|
|
23
18
|
|
|
24
19
|
output
|
|
25
20
|
end
|
|
21
|
+
|
|
22
|
+
class << self
|
|
23
|
+
def match?(segment:)
|
|
24
|
+
segment[:slot]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def build(segment:, **)
|
|
28
|
+
new(name: segment[:slot])
|
|
29
|
+
end
|
|
30
|
+
end
|
|
26
31
|
end
|
|
27
32
|
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.
|
|
4
|
+
version: 0.9.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- maedi
|
|
@@ -51,6 +51,20 @@ dependencies:
|
|
|
51
51
|
- - ">="
|
|
52
52
|
- !ruby/object:Gem::Version
|
|
53
53
|
version: '0'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: plugs
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - ">="
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '0'
|
|
61
|
+
type: :runtime
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '0'
|
|
54
68
|
description: A templating language embedded within HTML that is itself embedded within
|
|
55
69
|
Ruby/RBX
|
|
56
70
|
email:
|
|
@@ -60,7 +74,10 @@ extensions: []
|
|
|
60
74
|
extra_rdoc_files: []
|
|
61
75
|
files:
|
|
62
76
|
- lib/antlers.rb
|
|
63
|
-
- lib/
|
|
77
|
+
- lib/antlers/elements.rb
|
|
78
|
+
- lib/antlers/lexer.rb
|
|
79
|
+
- lib/antlers/parser.rb
|
|
80
|
+
- lib/antlers/version.rb
|
|
64
81
|
- lib/interfaces/antler_node.rb
|
|
65
82
|
- lib/interfaces/branch_node.rb
|
|
66
83
|
- lib/interfaces/leaf_node.rb
|
|
@@ -70,22 +87,21 @@ files:
|
|
|
70
87
|
- lib/lexemes/if_lexeme.rb
|
|
71
88
|
- lib/lexemes/prop_lexeme.rb
|
|
72
89
|
- lib/lexemes/slot_lexeme.rb
|
|
90
|
+
- lib/lexemes/var_lexeme.rb
|
|
73
91
|
- lib/lexemes/yield_lexeme.rb
|
|
74
|
-
- lib/lexer.rb
|
|
75
92
|
- lib/modules/namespace.rb
|
|
76
93
|
- lib/modules/props.rb
|
|
77
94
|
- lib/modules/variables.rb
|
|
78
95
|
- lib/nodes/for_node.rb
|
|
79
96
|
- lib/nodes/form_node.rb
|
|
97
|
+
- lib/nodes/html_node.rb
|
|
80
98
|
- lib/nodes/if_node.rb
|
|
81
99
|
- lib/nodes/prop_node.rb
|
|
82
100
|
- lib/nodes/root_node.rb
|
|
83
101
|
- lib/nodes/slot_node.rb
|
|
84
102
|
- lib/nodes/var_node.rb
|
|
85
103
|
- lib/nodes/yield_node.rb
|
|
86
|
-
- lib/parser.rb
|
|
87
104
|
- lib/support/queries.rb
|
|
88
|
-
- lib/version.rb
|
|
89
105
|
homepage: https://github.com/raindeer-rb/antlers
|
|
90
106
|
licenses: []
|
|
91
107
|
metadata:
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative '../nodes/if_node'
|
|
4
|
-
require_relative '../nodes/for_node'
|
|
5
|
-
require_relative '../nodes/form_node'
|
|
6
|
-
require_relative '../nodes/prop_node'
|
|
7
|
-
require_relative '../nodes/slot_node'
|
|
8
|
-
require_relative '../nodes/var_node'
|
|
9
|
-
require_relative '../nodes/yield_node'
|
|
10
|
-
|
|
11
|
-
module Antlers
|
|
12
|
-
class NodeFactory
|
|
13
|
-
def initialize(namespace:)
|
|
14
|
-
@namespace = namespace
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
def if_node(segment:)
|
|
18
|
-
IfNode.new(name: segment[:if_def], value: segment[:if_def])
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
def for_node(segment:)
|
|
22
|
-
value, key, items = segment.values_at(:for_def, :key, :in)
|
|
23
|
-
ForNode.new(name: value, key:, value:, items:)
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
def form_node(segment:)
|
|
27
|
-
action, method = segment.values_at(:form_def, :method)
|
|
28
|
-
return FormNode.new(name: action, action:, method:) if method
|
|
29
|
-
|
|
30
|
-
FormNode.new(name: action, action:)
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def prop_node(segment:)
|
|
34
|
-
PropNode.new(name: segment[:prop], props: segment[:props], namespace:)
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
def slot_node(segment:)
|
|
38
|
-
SlotNode.new(name: segment[:slot_def], props: segment[:props], namespace:)
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
def var_node(segment:)
|
|
42
|
-
VarNode.new(value: segment[:var])
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
def raw_var_node(segment:)
|
|
46
|
-
VarNode.new(value: segment[:raw_var], raw: true)
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
def yield_node(segment:)
|
|
50
|
-
YieldNode.new(name: segment[:slot])
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
private
|
|
54
|
-
|
|
55
|
-
attr_reader :namespace
|
|
56
|
-
end
|
|
57
|
-
end
|
data/lib/lexer.rb
DELETED
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative 'lexemes/for_lexeme'
|
|
4
|
-
require_relative 'lexemes/form_lexeme'
|
|
5
|
-
require_relative 'lexemes/if_lexeme'
|
|
6
|
-
require_relative 'lexemes/prop_lexeme'
|
|
7
|
-
require_relative 'lexemes/slot_lexeme'
|
|
8
|
-
require_relative 'lexemes/yield_lexeme'
|
|
9
|
-
require_relative 'support/queries'
|
|
10
|
-
|
|
11
|
-
module Antlers
|
|
12
|
-
extend Queries
|
|
13
|
-
|
|
14
|
-
class LexerParseError < StandardError; end
|
|
15
|
-
|
|
16
|
-
LEXEMES = [
|
|
17
|
-
# FormLexeme must be ordered before ForLexeme for regex to work.
|
|
18
|
-
Antlers::FormLexeme,
|
|
19
|
-
Antlers::ForLexeme,
|
|
20
|
-
Antlers::IfLexeme,
|
|
21
|
-
Antlers::PropLexeme,
|
|
22
|
-
Antlers::SlotLexeme,
|
|
23
|
-
Antlers::YieldLexeme,
|
|
24
|
-
]
|
|
25
|
-
|
|
26
|
-
class Lexer
|
|
27
|
-
def initialize
|
|
28
|
-
@delimiters = ['<{', '}>', '{', '}']
|
|
29
|
-
@keywords = LEXEMES.flat_map { |lexeme| lexeme.const_get(:KEYWORDS) }
|
|
30
|
-
@cursor = 0
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def parse(template)
|
|
34
|
-
@cursor = 0
|
|
35
|
-
sequence = []
|
|
36
|
-
|
|
37
|
-
# Split on delimiters and retain capture groups.
|
|
38
|
-
segments = template.split(/(#{Regexp.union(@delimiters)})/).map(&:strip)
|
|
39
|
-
|
|
40
|
-
until segments[@cursor].nil?
|
|
41
|
-
if (antlers_segment = antlers_segment(segments:))
|
|
42
|
-
sequence << antlers_lexeme(antlers_segment:, segments:)
|
|
43
|
-
# Skipping: ['{', 'expression', '}']
|
|
44
|
-
# Skipping: ['<{', 'name + props + keywords', '}>']
|
|
45
|
-
@cursor += 3
|
|
46
|
-
else
|
|
47
|
-
segment = segments[@cursor]
|
|
48
|
-
sequence << segment unless segment.empty?
|
|
49
|
-
@cursor += 1
|
|
50
|
-
end
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
sequence
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
private
|
|
57
|
-
|
|
58
|
-
def antlers_segment(segments:)
|
|
59
|
-
next_segment = segments[@cursor + 1]
|
|
60
|
-
return nil unless next_segment && (segments[@cursor] == '<{' || brackets?(segments:))
|
|
61
|
-
|
|
62
|
-
next_segment
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
def antlers_lexeme(antlers_segment:, segments:)
|
|
66
|
-
return var(antlers_segment:) if brackets?(segments:)
|
|
67
|
-
|
|
68
|
-
name, props, keywords = parse_segment(antlers_segment:)
|
|
69
|
-
|
|
70
|
-
lexeme = LEXEMES.find { it.match?(name:, keywords:) }
|
|
71
|
-
return lexeme.lexeme(name:, props:, keywords:) if lexeme
|
|
72
|
-
return var(antlers_segment:, raw: true) if deerheads?(segments:)
|
|
73
|
-
|
|
74
|
-
raise LexerParseError, "Unrecognised syntax: '#{antlers_segment}'"
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
def parse_segment(antlers_segment:)
|
|
78
|
-
name_and_props, *keywords = antlers_segment.split(/(#{Regexp.union(@keywords)})/)
|
|
79
|
-
name, *props = name_and_props.split(' ')
|
|
80
|
-
[name, props, parse_keywords(keywords:)]
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
def parse_keywords(keywords:)
|
|
84
|
-
key_values = {}
|
|
85
|
-
|
|
86
|
-
while (keyword = keywords.shift)
|
|
87
|
-
keyword.strip!
|
|
88
|
-
value = keyword.end_with?(':') && value?(keywords.first.strip) ? keywords.shift.strip : nil
|
|
89
|
-
key_values[keyword] = value
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
key_values
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
def value?(string)
|
|
96
|
-
!(string.start_with?(':') || string.end_with?(':'))
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
# TODO: Refactor every constant, match and result method into its own class. Loop through every class and return the first match.
|
|
100
|
-
|
|
101
|
-
def brackets?(segments:)
|
|
102
|
-
first, _, last = segments[@cursor..@cursor + 3].map(&:strip)
|
|
103
|
-
first == '{' && last == '}'
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
def deerheads?(segments:)
|
|
107
|
-
first, _, last = segments[@cursor..@cursor + 3].map(&:strip)
|
|
108
|
-
first == '<{' && last == '}>'
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
def var(antlers_segment:, raw: false)
|
|
112
|
-
# String is already interpolated or not depending on user input on the template layer, now we store it without those template quotes.
|
|
113
|
-
antlers_segment = antlers_segment[1..-2] if Queries.user_defined_string?(antlers_segment)
|
|
114
|
-
|
|
115
|
-
return { raw_var: antlers_segment } if raw
|
|
116
|
-
|
|
117
|
-
{ var: antlers_segment }
|
|
118
|
-
end
|
|
119
|
-
end
|
|
120
|
-
end
|
data/lib/parser.rb
DELETED
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative 'factories/node_factory'
|
|
4
|
-
require_relative 'nodes/root_node'
|
|
5
|
-
|
|
6
|
-
module Antlers
|
|
7
|
-
class Parser
|
|
8
|
-
def initialize(namespace: nil)
|
|
9
|
-
@factory = NodeFactory.new(namespace:)
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
def parse(sequence:, id: :root_node)
|
|
13
|
-
branch(node: RootNode.new(name: id), sequence:)
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def branch(node:, sequence:) # rubocop:disable Metrics/AbcSize
|
|
17
|
-
until sequence.empty?
|
|
18
|
-
segment = sequence.shift
|
|
19
|
-
|
|
20
|
-
if segment.is_a?(String)
|
|
21
|
-
node.children << segment
|
|
22
|
-
elsif segment[:var]
|
|
23
|
-
node.children << @factory.var_node(segment:)
|
|
24
|
-
elsif segment[:raw_var]
|
|
25
|
-
node.children << @factory.raw_var_node(segment:)
|
|
26
|
-
elsif segment[:prop]
|
|
27
|
-
node.children << @factory.prop_node(segment:)
|
|
28
|
-
elsif segment[:slot]
|
|
29
|
-
node.children << @factory.yield_node(segment:)
|
|
30
|
-
elsif segment[:slot_def]
|
|
31
|
-
slot_node = @factory.slot_node(segment:)
|
|
32
|
-
node.children << slot_node
|
|
33
|
-
sub_branch(node: slot_node, sequence:, end_key: :slot_end, end_name: slot_node.name)
|
|
34
|
-
elsif segment[:if_def]
|
|
35
|
-
if_node = @factory.if_node(segment:)
|
|
36
|
-
node.children << if_node
|
|
37
|
-
sub_branch(node: if_node, sequence:, end_key: :if_end, end_name: 'level_1')
|
|
38
|
-
elsif segment[:for_def]
|
|
39
|
-
for_node = @factory.for_node(segment:)
|
|
40
|
-
node.children << for_node
|
|
41
|
-
sub_branch(node: for_node, sequence:, end_key: :for_end, end_name: 'level_1')
|
|
42
|
-
elsif segment[:form_def]
|
|
43
|
-
form_node = @factory.form_node(segment:)
|
|
44
|
-
node.children << form_node
|
|
45
|
-
sub_branch(node: form_node, sequence:, end_key: :form_end, end_name: 'level_1')
|
|
46
|
-
end
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
node
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
def sub_branch(node:, sequence:, end_key:, end_name: nil)
|
|
53
|
-
sub_sequence = []
|
|
54
|
-
sub_sequence << sequence.shift until sequence.first.is_a?(Hash) && sequence.first[end_key] == end_name
|
|
55
|
-
|
|
56
|
-
branch(node:, sequence: sub_sequence)
|
|
57
|
-
end
|
|
58
|
-
end
|
|
59
|
-
end
|