tdrb 1.0.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 +7 -0
- data/lib/tdrb/error.rb +16 -0
- data/lib/tdrb/lexer.rb +83 -0
- data/lib/tdrb/parse.rb +89 -0
- data/lib/tdrb/parser.rb +220 -0
- data/lib/tdrb/performance.rb +57 -0
- data/lib/tdrb/result.rb +50 -0
- data/lib/tdrb/token.rb +63 -0
- data/lib/tdrb/version.rb +3 -0
- data/lib/tdrb.rb +4 -0
- metadata +54 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 2e92d32f3edc5319cab952f4777c42c97170325cd9e19c94558dd9f6cf36d965
|
4
|
+
data.tar.gz: 86954cb680c46b56814d95511d52a371fcfb2062fb715cdf8b7addc1d426e31e
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 20e77bb2e93ddd65de9b9f72df08f19711152cdf47c38b9dc257b59b31508497bbb8b7ecec32259a62518929c489c67817e94d3115d7f40b274f33b77fd4739c
|
7
|
+
data.tar.gz: 13bbcf53226ef1afb1d2273c2ad4d8799fde2cea64f04d62b8d973477ed5bb4bf061c15a79be294048af58b25b0daba250035c2f5ef02b8683bef5779ddaf0b9
|
data/lib/tdrb/error.rb
ADDED
data/lib/tdrb/lexer.rb
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
module TDRB
|
2
|
+
class Lexer
|
3
|
+
attr_reader :source, :tokens
|
4
|
+
|
5
|
+
def initialize(source:, state: -> (_) {})
|
6
|
+
@col = 0
|
7
|
+
@line = 0
|
8
|
+
@pos = 0
|
9
|
+
@source = source
|
10
|
+
@stack = []
|
11
|
+
@start = 0
|
12
|
+
@state = state
|
13
|
+
@tokens = []
|
14
|
+
end
|
15
|
+
|
16
|
+
def chars
|
17
|
+
@chars ||= source.grapheme_clusters
|
18
|
+
end
|
19
|
+
|
20
|
+
def current
|
21
|
+
chars[@start..]
|
22
|
+
.take(@pos - @start)
|
23
|
+
.join
|
24
|
+
end
|
25
|
+
|
26
|
+
def emit(token_type)
|
27
|
+
tokens << Token.new(literal: current, type: token_type)
|
28
|
+
|
29
|
+
@start = @pos
|
30
|
+
@stack = []
|
31
|
+
end
|
32
|
+
|
33
|
+
def ignore
|
34
|
+
@stack = []
|
35
|
+
@start = @pos
|
36
|
+
end
|
37
|
+
|
38
|
+
def next_char
|
39
|
+
return @stack << Token::EOF && Token::EOF if chars.count.pred == @pos
|
40
|
+
|
41
|
+
sub_source = source[@pos..]
|
42
|
+
@pos += 1
|
43
|
+
|
44
|
+
return @stack << Token::EOF && Token::EOF if sub_source.nil?
|
45
|
+
|
46
|
+
@stack << sub_source[0] && sub_source[0]
|
47
|
+
end
|
48
|
+
|
49
|
+
def peek
|
50
|
+
char = next_char
|
51
|
+
rewind
|
52
|
+
|
53
|
+
char
|
54
|
+
end
|
55
|
+
|
56
|
+
def pop_stack
|
57
|
+
@stack.pop || Token::EOF
|
58
|
+
end
|
59
|
+
|
60
|
+
def rewind
|
61
|
+
char = pop_stack
|
62
|
+
@pos -= 1 if char != Token::EOF
|
63
|
+
@pos = @start if (@pos < @start)
|
64
|
+
end
|
65
|
+
|
66
|
+
def run
|
67
|
+
while @state do
|
68
|
+
@state = @state.call(self)
|
69
|
+
end
|
70
|
+
|
71
|
+
true
|
72
|
+
end
|
73
|
+
|
74
|
+
def take(characters)
|
75
|
+
char = next_char
|
76
|
+
while characters.include?(char) do
|
77
|
+
char = next_char
|
78
|
+
end
|
79
|
+
|
80
|
+
rewind
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
data/lib/tdrb/parse.rb
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
require "time"
|
2
|
+
|
3
|
+
module TDRB
|
4
|
+
## Returns Array<Result> to allow for multisession documents.
|
5
|
+
def self.parse(traindown:)
|
6
|
+
metakey_buf = nil
|
7
|
+
movement_meta = {}
|
8
|
+
movement_notes = []
|
9
|
+
result = nil
|
10
|
+
performance = nil
|
11
|
+
|
12
|
+
Parser
|
13
|
+
.new(traindown: traindown)
|
14
|
+
.tokens
|
15
|
+
.each_with_object([]) do |token, arr|
|
16
|
+
case token.type
|
17
|
+
when Token::DATETIME
|
18
|
+
arr << result if !result.nil?
|
19
|
+
|
20
|
+
occurred_at = safe_time(token.literal)
|
21
|
+
result = Result.new(occurred_at: occurred_at)
|
22
|
+
when Token::FAIL
|
23
|
+
fails = safe_float(token.literal)
|
24
|
+
|
25
|
+
(performance || Performance.new).fails = fails
|
26
|
+
when Token::LOAD
|
27
|
+
if performance.nil? || performance.load
|
28
|
+
result.add_performance(performance) if performance&.load
|
29
|
+
|
30
|
+
performance =
|
31
|
+
Performance.new(
|
32
|
+
movement: performance&.movement,
|
33
|
+
load: safe_float(token.literal),
|
34
|
+
)
|
35
|
+
else
|
36
|
+
performance.load = safe_float(token.literal)
|
37
|
+
end
|
38
|
+
when Token::METAKEY
|
39
|
+
metakey_buf = token.literal
|
40
|
+
target = performance.nil? ? result.metadata : movement_meta
|
41
|
+
target[metakey_buf] = ""
|
42
|
+
when Token::METAVALUE
|
43
|
+
target = performance.nil? ? result.metadata : movement_meta
|
44
|
+
target[metakey_buf] = token.literal
|
45
|
+
metakey_buf = nil
|
46
|
+
when Token::MOVEMENT
|
47
|
+
if !performance.nil?
|
48
|
+
result
|
49
|
+
.add_performance(
|
50
|
+
performance,
|
51
|
+
metadata: movement_metadata,
|
52
|
+
notes: movement_notes,
|
53
|
+
)
|
54
|
+
end
|
55
|
+
|
56
|
+
performance = Performance.new(movement: token.literal)
|
57
|
+
when Token::NOTE
|
58
|
+
target = performance.nil? ? result.notes : movement_notes
|
59
|
+
target << token.literal
|
60
|
+
when Token::REP
|
61
|
+
reps = safe_float(token.literal)
|
62
|
+
|
63
|
+
(performance || Performance.new).reps = reps
|
64
|
+
when Token::SET
|
65
|
+
sets = safe_float(token.literal)
|
66
|
+
|
67
|
+
(performance || Performance.new).sets = sets
|
68
|
+
when Token::SUPERSET
|
69
|
+
end
|
70
|
+
end.tap do |results|
|
71
|
+
return results if result.nil?
|
72
|
+
|
73
|
+
result.add_performance(performance) if !performance.nil?
|
74
|
+
results << result
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def self.safe_float(maybe_float_str)
|
79
|
+
Float(maybe_float_str)
|
80
|
+
rescue
|
81
|
+
0.0
|
82
|
+
end
|
83
|
+
|
84
|
+
def self.safe_time(maybe_time_str)
|
85
|
+
Time.parse(maybe_time_str)
|
86
|
+
rescue
|
87
|
+
Time.now
|
88
|
+
end
|
89
|
+
end
|
data/lib/tdrb/parser.rb
ADDED
@@ -0,0 +1,220 @@
|
|
1
|
+
module TDRB
|
2
|
+
##
|
3
|
+
# Parser is the thing that does STUFF to a Token stream. What stuff? Well,
|
4
|
+
# that is defined by the handler lambdas.
|
5
|
+
|
6
|
+
class Parser
|
7
|
+
attr_reader :lexer, :traindown
|
8
|
+
|
9
|
+
def initialize(traindown:)
|
10
|
+
@lexer = Lexer.new(source: traindown, state: IDLE)
|
11
|
+
@traindown = traindown
|
12
|
+
end
|
13
|
+
|
14
|
+
def tokens
|
15
|
+
lexer.run
|
16
|
+
lexer.tokens
|
17
|
+
end
|
18
|
+
|
19
|
+
class << self
|
20
|
+
def colon_terminator?(char)
|
21
|
+
char == ":" || char == Token::EOF
|
22
|
+
end
|
23
|
+
|
24
|
+
def line_terminator?(char)
|
25
|
+
return true if char == Token::EOF
|
26
|
+
|
27
|
+
cp = char.codepoints;
|
28
|
+
|
29
|
+
return true if cp.empty?
|
30
|
+
return true if cp.count == 1 && [0, 10, 13, 59].include?(cp.first)
|
31
|
+
return true if cp.count == 2 && cp.first == 13 and cp.last == 10
|
32
|
+
|
33
|
+
false
|
34
|
+
end
|
35
|
+
|
36
|
+
def whitespace?(char)
|
37
|
+
char.strip.empty?
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
IDLE = lambda do |lexer|
|
42
|
+
char = lexer.peek
|
43
|
+
return if char == Token::EOF
|
44
|
+
|
45
|
+
if whitespace?(char) || line_terminator?(char)
|
46
|
+
lexer.next_char
|
47
|
+
lexer.ignore
|
48
|
+
|
49
|
+
return IDLE
|
50
|
+
end
|
51
|
+
|
52
|
+
case char
|
53
|
+
when "@" then DATETIME
|
54
|
+
when "#" then METAKEY
|
55
|
+
when "*" then NOTE
|
56
|
+
else
|
57
|
+
VALUE
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
DATETIME = lambda do |lexer|
|
62
|
+
lexer.take(["@", " "])
|
63
|
+
lexer.ignore
|
64
|
+
|
65
|
+
char = lexer.next_char
|
66
|
+
|
67
|
+
while !line_terminator?(char) do
|
68
|
+
char = lexer.next_char
|
69
|
+
end
|
70
|
+
|
71
|
+
lexer.rewind
|
72
|
+
lexer.emit(Token::DATETIME)
|
73
|
+
|
74
|
+
IDLE
|
75
|
+
end
|
76
|
+
|
77
|
+
METAKEY = lambda do |lexer|
|
78
|
+
lexer.take(["#", " "])
|
79
|
+
lexer.ignore
|
80
|
+
|
81
|
+
char = lexer.next_char
|
82
|
+
|
83
|
+
while !colon_terminator?(char) do
|
84
|
+
char = lexer.next_char
|
85
|
+
end
|
86
|
+
|
87
|
+
lexer.rewind
|
88
|
+
lexer.emit(Token::METAKEY)
|
89
|
+
|
90
|
+
METAVALUE
|
91
|
+
end
|
92
|
+
|
93
|
+
METAVALUE = lambda do |lexer|
|
94
|
+
lexer.take([":", " "])
|
95
|
+
lexer.ignore
|
96
|
+
|
97
|
+
char = lexer.next_char
|
98
|
+
|
99
|
+
while !line_terminator?(char) do
|
100
|
+
char = lexer.next_char
|
101
|
+
end
|
102
|
+
|
103
|
+
lexer.rewind
|
104
|
+
lexer.emit(Token::METAVALUE)
|
105
|
+
|
106
|
+
IDLE
|
107
|
+
end
|
108
|
+
|
109
|
+
# register_state :movement do
|
110
|
+
# end
|
111
|
+
|
112
|
+
MOVEMENT = lambda do |lexer|
|
113
|
+
superset = false;
|
114
|
+
|
115
|
+
char = lexer.next_char
|
116
|
+
|
117
|
+
return if char == Token::EOF
|
118
|
+
|
119
|
+
if char == "+"
|
120
|
+
superset = true
|
121
|
+
lexer.take([" "])
|
122
|
+
lexer.ignore
|
123
|
+
char = lexer.next_char
|
124
|
+
end
|
125
|
+
|
126
|
+
if char == "'"
|
127
|
+
lexer.ignore
|
128
|
+
char = lexer.next_char
|
129
|
+
end
|
130
|
+
|
131
|
+
while !colon_terminator?(char) do
|
132
|
+
char = lexer.next_char
|
133
|
+
end
|
134
|
+
|
135
|
+
lexer.rewind
|
136
|
+
|
137
|
+
if (superset)
|
138
|
+
lexer.emit(Token::SUPERSET)
|
139
|
+
else
|
140
|
+
lexer.emit(Token::MOVEMENT)
|
141
|
+
end
|
142
|
+
|
143
|
+
lexer.take([":"])
|
144
|
+
lexer.ignore
|
145
|
+
|
146
|
+
IDLE
|
147
|
+
end
|
148
|
+
|
149
|
+
NOTE = lambda do |lexer|
|
150
|
+
lexer.take(["*", " "])
|
151
|
+
lexer.ignore
|
152
|
+
|
153
|
+
char = lexer.next_char
|
154
|
+
|
155
|
+
while !line_terminator?(char) do
|
156
|
+
char = lexer.next_char
|
157
|
+
end
|
158
|
+
|
159
|
+
lexer.rewind
|
160
|
+
lexer.emit(Token::NOTE)
|
161
|
+
|
162
|
+
IDLE
|
163
|
+
end
|
164
|
+
|
165
|
+
NUMBER = lambda do |lexer|
|
166
|
+
lexer.take(["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "."])
|
167
|
+
|
168
|
+
# TODO: Configurable chars
|
169
|
+
case lexer.peek
|
170
|
+
when "f", "F" then lexer.emit(Token::FAIL)
|
171
|
+
when "r", "R" then lexer.emit(Token::REP)
|
172
|
+
when "s", "S" then lexer.emit(TokenType::SET)
|
173
|
+
else
|
174
|
+
lexer.emit(Token::LOAD)
|
175
|
+
end
|
176
|
+
|
177
|
+
lexer.take(["f", "F", "r", "R", "s", "S"])
|
178
|
+
lexer.ignore
|
179
|
+
|
180
|
+
IDLE
|
181
|
+
end
|
182
|
+
|
183
|
+
VALUE = lambda do |lexer|
|
184
|
+
char = lexer.next_char
|
185
|
+
|
186
|
+
if char == "+" || char == "'"
|
187
|
+
lexer.rewind
|
188
|
+
return MOVEMENT
|
189
|
+
end
|
190
|
+
|
191
|
+
begin
|
192
|
+
!!Integer(char)
|
193
|
+
return NUMBER
|
194
|
+
rescue ArgumentError, TypeError
|
195
|
+
nil
|
196
|
+
end
|
197
|
+
|
198
|
+
if char != "b" && char != "B"
|
199
|
+
lexer.rewind
|
200
|
+
return MOVEMENT
|
201
|
+
end
|
202
|
+
|
203
|
+
peek = lexer.peek
|
204
|
+
|
205
|
+
if peek != "w" && peek != "W"
|
206
|
+
lexer.rewind
|
207
|
+
MOVEMENT
|
208
|
+
end
|
209
|
+
|
210
|
+
while !whitespace?(char)
|
211
|
+
char = lexer.next_char
|
212
|
+
end
|
213
|
+
|
214
|
+
lexer.rewind
|
215
|
+
lexer.emit(Token::LOAD)
|
216
|
+
|
217
|
+
IDLE
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module TDRB
|
2
|
+
class Performance
|
3
|
+
attr_accessor :fails,
|
4
|
+
:load,
|
5
|
+
:metadata,
|
6
|
+
:movement,
|
7
|
+
:notes,
|
8
|
+
:sequence,
|
9
|
+
:sets,
|
10
|
+
:reps
|
11
|
+
|
12
|
+
def initialize(
|
13
|
+
fails: 0,
|
14
|
+
load: nil,
|
15
|
+
movement: "Unidentified Movement",
|
16
|
+
metadata: {},
|
17
|
+
notes: [],
|
18
|
+
reps: 1,
|
19
|
+
sequence: 0,
|
20
|
+
sets: 1
|
21
|
+
)
|
22
|
+
@fails = 0
|
23
|
+
@load = load
|
24
|
+
@movement = movement
|
25
|
+
@metadata = metadata
|
26
|
+
@notes = notes
|
27
|
+
@reps = 1
|
28
|
+
@sequence = 0
|
29
|
+
@sets = 1
|
30
|
+
end
|
31
|
+
|
32
|
+
def ==(p)
|
33
|
+
raise TypeError.new("Not a Performance") if !p.is_a? Performance
|
34
|
+
|
35
|
+
p.movement == movement &&
|
36
|
+
p.reps == reps &&
|
37
|
+
p.sets == sets &&
|
38
|
+
p.fails == fails &&
|
39
|
+
p.load == load
|
40
|
+
end
|
41
|
+
|
42
|
+
def to_h
|
43
|
+
@hash ||=
|
44
|
+
instance_variables.each_with_object({}) do |iv, h|
|
45
|
+
h[iv[1..].to_sym] = instance_variable_get(iv)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def to_json
|
50
|
+
@json ||= to_h.to_json
|
51
|
+
end
|
52
|
+
|
53
|
+
def volume
|
54
|
+
(load || 0.0) * sets * (reps - fails)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
data/lib/tdrb/result.rb
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
require "json"
|
2
|
+
require "set"
|
3
|
+
|
4
|
+
module TDRB
|
5
|
+
class Result
|
6
|
+
attr_reader :metadata,
|
7
|
+
:notes,
|
8
|
+
:movements,
|
9
|
+
:occurred_at,
|
10
|
+
:performances,
|
11
|
+
:source
|
12
|
+
|
13
|
+
def initialize(
|
14
|
+
movements: ::Set.new,
|
15
|
+
occurred_at: nil,
|
16
|
+
performances: [],
|
17
|
+
source: ""
|
18
|
+
)
|
19
|
+
@metadata = {}
|
20
|
+
@movements = movements
|
21
|
+
@notes = []
|
22
|
+
@occurred_at = occurred_at
|
23
|
+
@performances = performances
|
24
|
+
@source = source
|
25
|
+
end
|
26
|
+
|
27
|
+
def add_performance(performance, metadata: {}, notes: [])
|
28
|
+
raise TypeError.new("Not a performance") if !performance.is_a? Performance
|
29
|
+
|
30
|
+
movements.add(performance.movement)
|
31
|
+
|
32
|
+
performance.metadata = metadata
|
33
|
+
performance.notes = notes
|
34
|
+
performance.sequence = performances.count
|
35
|
+
|
36
|
+
performances << performance
|
37
|
+
end
|
38
|
+
|
39
|
+
def to_h
|
40
|
+
@hash ||=
|
41
|
+
instance_variables.each_with_object({}) do |iv, h|
|
42
|
+
h[iv[1..].to_sym] = instance_variable_get(iv)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def to_json
|
47
|
+
@json ||= to_h.to_json
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
data/lib/tdrb/token.rb
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
module TDRB
|
2
|
+
class Token
|
3
|
+
attr_reader :literal, :type
|
4
|
+
|
5
|
+
DATETIME = "Date / Time".freeze
|
6
|
+
FAIL = "Fails".freeze
|
7
|
+
LOAD = "Load".freeze
|
8
|
+
METAKEY = "Metadata Key".freeze
|
9
|
+
METAVALUE = "Metadata Value".freeze
|
10
|
+
MOVEMENT = "Movement".freeze
|
11
|
+
NOTE = "Note".freeze
|
12
|
+
REP = "Reps".freeze
|
13
|
+
SET = "Sets".freeze
|
14
|
+
SUPERSET = "Superset Movement".freeze
|
15
|
+
|
16
|
+
TOKEN_SYMBOLS = %i[
|
17
|
+
datetime fail load metakey metavalue movement note rep set superset
|
18
|
+
]
|
19
|
+
|
20
|
+
ALLOWED_TYPES = TOKEN_SYMBOLS.map { |ts| const_get(ts.upcase) }.freeze
|
21
|
+
|
22
|
+
EOF = "\x00".freeze
|
23
|
+
|
24
|
+
class << self
|
25
|
+
TOKEN_SYMBOLS.each do |method|
|
26
|
+
define_method(method) do |literal|
|
27
|
+
new(literal: literal, type: const_get(method.upcase))
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def initialize(literal:, type:)
|
33
|
+
raise TokenTypeError if !ALLOWED_TYPES.include?(type)
|
34
|
+
|
35
|
+
@literal = literal
|
36
|
+
@type = type
|
37
|
+
end
|
38
|
+
|
39
|
+
def ==(t)
|
40
|
+
raise TokenArgsError.new("Invalid comparison") if !t.is_a? Token
|
41
|
+
|
42
|
+
literal == t.literal && type == t.type
|
43
|
+
end
|
44
|
+
|
45
|
+
def ===(t)
|
46
|
+
raise TokenTypeError if !ALLOWED_TYPES.include?(t)
|
47
|
+
|
48
|
+
type == t
|
49
|
+
end
|
50
|
+
|
51
|
+
def movement?
|
52
|
+
@movement ||= [MOVEMENT, SUPERSET].include?(type)
|
53
|
+
end
|
54
|
+
|
55
|
+
def performance?
|
56
|
+
@performance ||= [FAIL, LOAD, REP, SET].include?(type)
|
57
|
+
end
|
58
|
+
|
59
|
+
def to_s
|
60
|
+
"[#{type}] #{literal}"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
data/lib/tdrb/version.rb
ADDED
data/lib/tdrb.rb
ADDED
metadata
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: tdrb
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Tyler Scott
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2021-12-14 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: A fun little gem chockful of Traindown tchotchkies
|
14
|
+
email: tyler@greaterscott.com
|
15
|
+
executables: []
|
16
|
+
extensions: []
|
17
|
+
extra_rdoc_files: []
|
18
|
+
files:
|
19
|
+
- lib/tdrb.rb
|
20
|
+
- lib/tdrb/error.rb
|
21
|
+
- lib/tdrb/lexer.rb
|
22
|
+
- lib/tdrb/parse.rb
|
23
|
+
- lib/tdrb/parser.rb
|
24
|
+
- lib/tdrb/performance.rb
|
25
|
+
- lib/tdrb/result.rb
|
26
|
+
- lib/tdrb/token.rb
|
27
|
+
- lib/tdrb/version.rb
|
28
|
+
homepage: https://traindown.com
|
29
|
+
licenses:
|
30
|
+
- BSD-3-Clause
|
31
|
+
metadata:
|
32
|
+
homepage_uri: https://traindown.com
|
33
|
+
source_code_uri: https://traindown.com
|
34
|
+
changelog_uri: https://traindown.com
|
35
|
+
post_install_message:
|
36
|
+
rdoc_options: []
|
37
|
+
require_paths:
|
38
|
+
- lib
|
39
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
40
|
+
requirements:
|
41
|
+
- - ">="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: 2.7.0
|
44
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
45
|
+
requirements:
|
46
|
+
- - ">="
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: '0'
|
49
|
+
requirements: []
|
50
|
+
rubygems_version: 3.1.2
|
51
|
+
signing_key:
|
52
|
+
specification_version: 4
|
53
|
+
summary: Ruby + Traindown = TDRB
|
54
|
+
test_files: []
|