antlers 0.0.0 → 0.1.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.rb +15 -0
- data/lib/factories/node_factory.rb +23 -0
- data/lib/interfaces/antler_node.rb +16 -0
- data/lib/lexer.rb +117 -0
- data/lib/nodes/prop_node.rb +15 -0
- data/lib/nodes/root_node.rb +15 -0
- data/lib/nodes/slot_node.rb +15 -0
- data/lib/nodes/var_node.rb +8 -0
- data/lib/parser.rb +38 -0
- data/lib/version.rb +1 -1
- metadata +10 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7aef5ead7628092869dbae7f4ae7fee7bea88a69e1c3ee2e9ebd5fd6ace600ea
|
|
4
|
+
data.tar.gz: a96ba7818b542305105305c9c7cbc73a6c4962b118e6f0686f1eb73cfda59a56
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ff1db9dd204501ae40f3ccc43ebf56b236aa1776ef233edd7a982d34ca5ad5d5486aaad29de46f6f382adc092cb54d5169a6a84443faae5799eb26cb7a9c96ee
|
|
7
|
+
data.tar.gz: 5637d1f3cb287c70b7f47d0cb7405c594b98fc3858a624e10302ef935e88872f07fa5a67403bbb091a7ec29af758161f8effa9c2055ddd3a12ec2aa32d1d470a
|
data/lib/antlers.rb
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Antlers
|
|
4
|
+
class << self
|
|
5
|
+
def parse(template)
|
|
6
|
+
return template unless template.include?('<{') || template.include?('{')
|
|
7
|
+
|
|
8
|
+
lexemes = Lexer.new.parse(template)
|
|
9
|
+
Parser.parse(lexemes)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# TODO: Render the AST.
|
|
13
|
+
def render(antler_node); end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../nodes/prop_node'
|
|
4
|
+
require_relative '../nodes/slot_node'
|
|
5
|
+
require_relative '../nodes/var_node'
|
|
6
|
+
|
|
7
|
+
module Antlers
|
|
8
|
+
class NodeFactory
|
|
9
|
+
class << self
|
|
10
|
+
def var_node(segment:)
|
|
11
|
+
VarNode.new(name: segment[:ivar])
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def prop_node(segment:)
|
|
15
|
+
PropNode.new(name: segment[:prop], props: segment[:props])
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def slot_node(segment:)
|
|
19
|
+
SlotNode.new(name: segment[:slot_def], props: segment[:props])
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Antlers
|
|
4
|
+
class AntlerNode
|
|
5
|
+
attr_reader :name
|
|
6
|
+
|
|
7
|
+
def initialize(name:)
|
|
8
|
+
@name = name
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Consider this a value object on comparison.
|
|
12
|
+
def ==(other) = other.class == self.class
|
|
13
|
+
def eql?(other) = self == other
|
|
14
|
+
def hash = [self.class].hash
|
|
15
|
+
end
|
|
16
|
+
end
|
data/lib/lexer.rb
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Antlers
|
|
4
|
+
class LexerParseError < StandardError; end
|
|
5
|
+
|
|
6
|
+
class Lexer
|
|
7
|
+
def initialize
|
|
8
|
+
@delimiters = ['<{', '}>', '{', '}']
|
|
9
|
+
@keywords = ['if:', 'for:', 'in:']
|
|
10
|
+
@cursor = 0
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def parse(template)
|
|
14
|
+
@cursor = 0
|
|
15
|
+
sequence = []
|
|
16
|
+
|
|
17
|
+
# Split on delimiters and retain capture groups.
|
|
18
|
+
segments = template.split(/(#{Regexp.union(@delimiters)})/).map(&:strip)
|
|
19
|
+
|
|
20
|
+
until segments[@cursor].nil?
|
|
21
|
+
if (antlers_segment = antlers_segment(segments:))
|
|
22
|
+
sequence << antlers_lexeme(antlers_segment:, segments:)
|
|
23
|
+
# Skipping: ['{', '@ivar', '}']
|
|
24
|
+
# Skipping: ['<{', 'name + props + keywords', '}>']
|
|
25
|
+
@cursor += 3
|
|
26
|
+
else
|
|
27
|
+
segment = segments[@cursor]
|
|
28
|
+
sequence << segment unless segment.empty?
|
|
29
|
+
@cursor += 1
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
sequence
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def antlers_segment(segments:)
|
|
39
|
+
next_segment = segments[@cursor + 1]
|
|
40
|
+
return nil unless next_segment && (segments[@cursor] == '<{' || ivar?(segments:))
|
|
41
|
+
|
|
42
|
+
next_segment
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def antlers_lexeme(antlers_segment:, segments:)
|
|
46
|
+
return ivar(antlers_segment:) if ivar?(segments:)
|
|
47
|
+
|
|
48
|
+
name, props, _keywords = parse_segment(antlers_segment:)
|
|
49
|
+
|
|
50
|
+
return slot(name:, props:) if slot?(name)
|
|
51
|
+
return prop(name:, props:) if prop?(name)
|
|
52
|
+
|
|
53
|
+
raise LexerParseError, "Couldn't parse antlers syntax: '#{antlers_segment}'"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def parse_segment(antlers_segment:)
|
|
57
|
+
name_and_props, *keywords = antlers_segment.split(/(#{Regexp.union(@keywords)})/)
|
|
58
|
+
name, *props = name_and_props.split(' ')
|
|
59
|
+
|
|
60
|
+
[name, props, keywords]
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def ivar?(segments:)
|
|
64
|
+
first, second, third = segments[@cursor..@cursor + 3].map(&:strip)
|
|
65
|
+
first == '{' && second&.start_with?('@') && third == '}'
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def slot?(name)
|
|
69
|
+
name.start_with?(':') || name.end_with?(':')
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def prop?(name)
|
|
73
|
+
[*'A'..'Z'].include?(name[0])
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def ivar(antlers_segment:)
|
|
77
|
+
{ ivar: antlers_segment.delete_prefix('@') }
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def slot(name:, props:)
|
|
81
|
+
if name.end_with?(':')
|
|
82
|
+
slot_def = { slot_def: name.delete_suffix(':') }
|
|
83
|
+
slot_def[:props] = props(props) unless props.empty?
|
|
84
|
+
return slot_def
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
{ slot_end: name.delete_prefix(':') }
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def prop(name:, props:)
|
|
91
|
+
prop = { prop: name }
|
|
92
|
+
prop[:props] = props(props) unless props.empty?
|
|
93
|
+
prop
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def props(props)
|
|
97
|
+
odd_props = props.join(' ').split(/(=)|\s/)
|
|
98
|
+
|
|
99
|
+
return {} unless odd_props.any?
|
|
100
|
+
|
|
101
|
+
props = {}
|
|
102
|
+
until odd_props.empty?
|
|
103
|
+
prop = odd_props.shift
|
|
104
|
+
value = nil
|
|
105
|
+
|
|
106
|
+
if odd_props.first == '='
|
|
107
|
+
odd_props.shift
|
|
108
|
+
value = odd_props.shift
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
props[prop] = value
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
props
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../interfaces/antler_node'
|
|
4
|
+
|
|
5
|
+
module Antlers
|
|
6
|
+
class RootNode < AntlerNode
|
|
7
|
+
attr_accessor :children
|
|
8
|
+
|
|
9
|
+
def initialize(name: :root_node, children: [])
|
|
10
|
+
super(name:)
|
|
11
|
+
|
|
12
|
+
@children = children
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../nodes/prop_node'
|
|
4
|
+
|
|
5
|
+
module Antlers
|
|
6
|
+
class SlotNode < PropNode
|
|
7
|
+
attr_accessor :children
|
|
8
|
+
|
|
9
|
+
def initialize(name:, props: [], children: [])
|
|
10
|
+
super(name:, props:)
|
|
11
|
+
|
|
12
|
+
@children = children
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
data/lib/parser.rb
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'factories/node_factory'
|
|
4
|
+
require_relative 'nodes/root_node'
|
|
5
|
+
|
|
6
|
+
module Antlers
|
|
7
|
+
module Parser
|
|
8
|
+
class << self
|
|
9
|
+
def parse(sequence, id: :root_node)
|
|
10
|
+
branch(branch_node: RootNode.new(name: id), sequence:)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def branch(branch_node:, sequence:) # rubocop:disable Metrics/AbcSize
|
|
14
|
+
until sequence.empty?
|
|
15
|
+
segment = sequence.shift
|
|
16
|
+
|
|
17
|
+
if segment.is_a?(String)
|
|
18
|
+
branch_node.children << segment
|
|
19
|
+
elsif segment[:ivar]
|
|
20
|
+
branch_node.children << NodeFactory.var_node(segment:)
|
|
21
|
+
elsif segment[:prop]
|
|
22
|
+
branch_node.children << NodeFactory.prop_node(segment:)
|
|
23
|
+
elsif segment[:slot_def]
|
|
24
|
+
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)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
branch_node
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
data/lib/version.rb
CHANGED
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.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- maedi
|
|
@@ -30,6 +30,15 @@ executables: []
|
|
|
30
30
|
extensions: []
|
|
31
31
|
extra_rdoc_files: []
|
|
32
32
|
files:
|
|
33
|
+
- lib/antlers.rb
|
|
34
|
+
- lib/factories/node_factory.rb
|
|
35
|
+
- lib/interfaces/antler_node.rb
|
|
36
|
+
- lib/lexer.rb
|
|
37
|
+
- lib/nodes/prop_node.rb
|
|
38
|
+
- lib/nodes/root_node.rb
|
|
39
|
+
- lib/nodes/slot_node.rb
|
|
40
|
+
- lib/nodes/var_node.rb
|
|
41
|
+
- lib/parser.rb
|
|
33
42
|
- lib/version.rb
|
|
34
43
|
homepage: https://codeberg.org/raindeer/antlers
|
|
35
44
|
licenses: []
|