scheduled-format 0.0.1
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/README.md +63 -0
- data/lib/scheduled-format.rb +37 -0
- data/lib/scheduled-format/parser/parser.rb +46 -0
- data/lib/scheduled-format/parser/transformer.rb +26 -0
- data/lib/scheduled-format/task.rb +39 -0
- data/lib/scheduled-format/task_group.rb +101 -0
- data/lib/scheduled-format/task_list.rb +106 -0
- metadata +78 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: c1fcfd8366043e95ad3620345cc368848c36e2b08b3c3671d3189f19696c7030
|
4
|
+
data.tar.gz: efe64d9385bbd041ac75ac18c10d094b73e490c038b0c742e554a0b3b8f10ecf
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 8b922da0a4298956a10e7fa8ca89b11ef5e945667bb6bac19d1ed3e9a3538402184789f35002e8b7e3a4735e074f39d80f629b433bd4ab363a264b9fce111619
|
7
|
+
data.tar.gz: 905a9715919ea3ff1dddbcd2adb3f11f076d06eb55351e5fd41350bd9e6ee664efd3325c014e3abb897d1170de1eebbe8b738feed84066fafd12ad8aa750f1ae
|
data/README.md
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
# About
|
2
|
+
|
3
|
+
[![Gem version][GV img]][Gem version]
|
4
|
+
[![Build status][BS img]][Build status]
|
5
|
+
[![Coverage status][CS img]][Coverage status]
|
6
|
+
[![CodeClimate status][CC img]][CodeClimate status]
|
7
|
+
[![YARD documentation][YD img]][YARD documentation]
|
8
|
+
|
9
|
+
This format is used to store **scheduled tasks**: tasks that will be done later
|
10
|
+
or in a certain context.
|
11
|
+
|
12
|
+
# API
|
13
|
+
|
14
|
+
```ruby
|
15
|
+
require 'import'
|
16
|
+
|
17
|
+
simple_format = import('simple-format')
|
18
|
+
simple_format.parse(File.read('tasks.todo'))
|
19
|
+
```
|
20
|
+
|
21
|
+
# Format
|
22
|
+
|
23
|
+
```
|
24
|
+
Tomorrow
|
25
|
+
- Buy milk. #errands
|
26
|
+
- [9:20] Call with Mike.
|
27
|
+
|
28
|
+
Prague
|
29
|
+
- Pick up my shoes. #errands
|
30
|
+
```
|
31
|
+
|
32
|
+
# Currently unsupported
|
33
|
+
|
34
|
+
- **Labels**. Labels allow us to match tasks with _named_ time frames.
|
35
|
+
See [#8](https://github.com/botanicus/now-task-manager/issues/8).
|
36
|
+
|
37
|
+
```
|
38
|
+
- ADM: Catch up with Eva.
|
39
|
+
```
|
40
|
+
|
41
|
+
# Intentionally unsupported
|
42
|
+
|
43
|
+
- **Comments**. I want to keep the format simple and the task file small.
|
44
|
+
Every time there was something like comments, the file bloated uncontrollably.
|
45
|
+
- **Task formatting**. Task is a string, it doesn't recognise any structures within.
|
46
|
+
Therefore, anything you can fit in to a line will be the task body. So you can
|
47
|
+
put anything that {Pomodoro::Formats::Today} supports such as scheduled times
|
48
|
+
and tags.
|
49
|
+
|
50
|
+
_For more details about the format see
|
51
|
+
[parser_spec.rb](https://github.com/botanicus/scheduled-format/blob/master/spec/scheduled-format/parser/parser_spec.rb)._
|
52
|
+
|
53
|
+
[Gem version]: https://rubygems.org/gems/scheduled-format
|
54
|
+
[Build status]: https://travis-ci.org/botanicus/scheduled-format
|
55
|
+
[Coverage status]: https://coveralls.io/github/botanicus/scheduled-format
|
56
|
+
[CodeClimate status]: https://codeclimate.com/github/botanicus/scheduled-format/maintainability
|
57
|
+
[YARD documentation]: http://www.rubydoc.info/github/botanicus/scheduled-format/master
|
58
|
+
|
59
|
+
[GV img]: https://badge.fury.io/rb/scheduled-format.svg
|
60
|
+
[BS img]: https://travis-ci.org/botanicus/scheduled-format.svg?branch=master
|
61
|
+
[CS img]: https://img.shields.io/coveralls/botanicus/scheduled-format.svg
|
62
|
+
[CC img]: https://api.codeclimate.com/v1/badges/a99a88d28ad37a79dbf6/maintainability
|
63
|
+
[YD img]: http://img.shields.io/badge/yard-docs-blue.svg
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'parslet'
|
2
|
+
require 'parslet/convenience' # parse_with_debug
|
3
|
+
|
4
|
+
Parser = import('scheduled-format/parser/parser')
|
5
|
+
Transformer = import('scheduled-format/parser/transformer')
|
6
|
+
TaskList = import('scheduled-format/task_list')
|
7
|
+
|
8
|
+
# {include:file:doc/formats/scheduled.md}
|
9
|
+
# The entry point method for parsing this format.
|
10
|
+
#
|
11
|
+
# @param string [String] string in the scheduled task list format
|
12
|
+
# @return [TaskList, nil]
|
13
|
+
#
|
14
|
+
# @example
|
15
|
+
# scheduled_format = import('scheduled-format')
|
16
|
+
#
|
17
|
+
# task_list = scheduled_format.parse <<-EOF.gsub(/^\s*/, '')
|
18
|
+
# Tomorrow
|
19
|
+
# - Buy milk. #errands
|
20
|
+
# - [9:20] Call with Mike.
|
21
|
+
#
|
22
|
+
# Prague
|
23
|
+
# - Pick up my shoes. #errands
|
24
|
+
# EOF
|
25
|
+
# @since 0.2
|
26
|
+
def exports.parse(string_or_io)
|
27
|
+
string = string_or_io.respond_to?(:read) ? string_or_io.read : string_or_io
|
28
|
+
tree = Parser.new.parse_with_debug(string)
|
29
|
+
nodes = Transformer.new.apply(tree)
|
30
|
+
TaskList.new(nodes.empty? ? Array.new : nodes)
|
31
|
+
end
|
32
|
+
|
33
|
+
export parser: Parser,
|
34
|
+
transformer: Transformer,
|
35
|
+
task: import('scheduled-format/task'),
|
36
|
+
task_list: TaskList,
|
37
|
+
task_group: import('scheduled-format/task_group')
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'parslet'
|
2
|
+
|
3
|
+
# @api private
|
4
|
+
class Parser < Parslet::Parser
|
5
|
+
rule(:hour_strict) {
|
6
|
+
(match['\d'].repeat(1) >> str(':') >> match['\d'].repeat(2, 2)).as(:hour)
|
7
|
+
}
|
8
|
+
|
9
|
+
rule(:start_time) {
|
10
|
+
str('[') >> hour_strict.as(:start_time) >> str(']') >> str(' ').maybe
|
11
|
+
}
|
12
|
+
|
13
|
+
rule(:time_frame) {
|
14
|
+
# TODO: (hour_strict.absent? >> match['^\]\n'] >> any)
|
15
|
+
str('[') >> match['\w '].repeat.as(:str) >> str(']') >> str(' ').maybe
|
16
|
+
}
|
17
|
+
|
18
|
+
rule(:header) {
|
19
|
+
# match['^\n'].repeat # This makes it hang!
|
20
|
+
(str("\n").absent? >> any).repeat(1).as(:str) >> str("\n")
|
21
|
+
}
|
22
|
+
|
23
|
+
rule(:task_body) do
|
24
|
+
(match['#\n'].absent? >> any).repeat.as(:str)
|
25
|
+
end
|
26
|
+
|
27
|
+
rule(:tag) do
|
28
|
+
str('#') >> match['^\s'].repeat.as(:str) >> str(' ').maybe
|
29
|
+
end
|
30
|
+
|
31
|
+
rule(:task) {
|
32
|
+
str('- ') >> (time_frame.as(:time_frame).maybe >> start_time.maybe >> task_body.as(:body) >> tag.as(:tag).repeat.as(:tags).maybe).as(:task) >> str("\n").repeat
|
33
|
+
}
|
34
|
+
|
35
|
+
rule(:task_group) {
|
36
|
+
(header.as(:header) >> task.repeat.as(:tasks)).as(:task_group)
|
37
|
+
}
|
38
|
+
|
39
|
+
rule(:task_groups) {
|
40
|
+
task_group.repeat(0)
|
41
|
+
}
|
42
|
+
|
43
|
+
root(:task_groups)
|
44
|
+
end
|
45
|
+
|
46
|
+
export { Parser }
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'parslet'
|
2
|
+
require 'refined-refinements/hour'
|
3
|
+
|
4
|
+
Task = import('scheduled-format/task')
|
5
|
+
TaskGroup = import('scheduled-format/task_group')
|
6
|
+
|
7
|
+
# @api private
|
8
|
+
class Transformer < Parslet::Transform
|
9
|
+
rule(str: simple(:slice)) { slice.to_s.strip }
|
10
|
+
|
11
|
+
rule(tag: simple(:slice)) { slice.to_sym }
|
12
|
+
|
13
|
+
rule(hour: simple(:hour_string)) do
|
14
|
+
Hour.parse(hour_string.to_s)
|
15
|
+
end
|
16
|
+
|
17
|
+
rule(task: subtree(:data)) do
|
18
|
+
Task.new(**data)
|
19
|
+
end
|
20
|
+
|
21
|
+
rule(task_group: subtree(:data)) {
|
22
|
+
TaskGroup.new(**data)
|
23
|
+
}
|
24
|
+
end
|
25
|
+
|
26
|
+
export { Transformer }
|
@@ -0,0 +1,39 @@
|
|
1
|
+
class Task
|
2
|
+
attr_reader :body, :time_frame, :start_time, :tags
|
3
|
+
|
4
|
+
# Create a new scheduled task.
|
5
|
+
#
|
6
|
+
# @param body [String] the task description.
|
7
|
+
# @param start_time [Hour] when the task starts.
|
8
|
+
# @param time_frame [String] name, initials or an abbreviation of a time frame
|
9
|
+
# to which the task will be scheduled.
|
10
|
+
# @param tags [Array<Symbol>] list of tags.
|
11
|
+
def initialize(body:, time_frame: nil, start_time: nil, tags: Array.new)
|
12
|
+
@body, @time_frame, @start_time, @tags = body, time_frame, start_time, tags
|
13
|
+
|
14
|
+
if body.empty?
|
15
|
+
raise ArgumentError.new("Body cannot be empty.")
|
16
|
+
end
|
17
|
+
|
18
|
+
if start_time && ! start_time.is_a?(Hour)
|
19
|
+
raise ArgumentError.new("Hour instance was expected, got #{start_time.class}")
|
20
|
+
end
|
21
|
+
|
22
|
+
if ! tags.empty? && tags.any? { |tag| ! tag.is_a?(Symbol) }
|
23
|
+
raise ArgumentError.new("Tags are supposed to be an array of symbols.")
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Format task in the {Pomodoro::Formats::Scheduled scheduled task list format}.
|
28
|
+
# @since 0.2
|
29
|
+
def to_s
|
30
|
+
[
|
31
|
+
'-',
|
32
|
+
("[#{@time_frame}]" if @time_frame),
|
33
|
+
("[#{@start_time}]" if @start_time),
|
34
|
+
"#{@body}", *@tags.map { |tag| "##{tag}"}
|
35
|
+
].compact.join(' ')
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
export { Task }
|
@@ -0,0 +1,101 @@
|
|
1
|
+
require 'date'
|
2
|
+
|
3
|
+
class TaskGroup
|
4
|
+
# @since 0.2
|
5
|
+
attr_reader :header, :tasks
|
6
|
+
|
7
|
+
# @param header [String] header of the task group.
|
8
|
+
# @param tasks [Array<String>] tasks of the group.
|
9
|
+
# @since 0.2
|
10
|
+
#
|
11
|
+
# @example
|
12
|
+
# TaskGroup = import('scheduled-format/task_group')
|
13
|
+
#
|
14
|
+
# tasks = ['Buy milk. #errands', '[9:20] Call with Mike.']
|
15
|
+
# group = TaskGroup.new(header: 'Tomorrow', tasks: tasks)
|
16
|
+
def initialize(header:, tasks: Array.new)
|
17
|
+
@header, @tasks = header, tasks
|
18
|
+
if tasks.any? { |task| ! task.is_a?(Task) }
|
19
|
+
raise ArgumentError.new("Task objects expected.")
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Add a task to the task group.
|
24
|
+
#
|
25
|
+
# @since 0.2
|
26
|
+
def <<(task)
|
27
|
+
unless task.is_a?(Task)
|
28
|
+
raise ArgumentError.new("Task expected, got #{task.class}.")
|
29
|
+
end
|
30
|
+
|
31
|
+
@tasks << task unless @tasks.map(&:to_s).include?(task.to_s)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Remove a task from the task group.
|
35
|
+
#
|
36
|
+
# @since 0.2
|
37
|
+
def delete(task)
|
38
|
+
unless task.is_a?(Task)
|
39
|
+
raise ArgumentError.new("Task expected, got #{task.class}.")
|
40
|
+
end
|
41
|
+
|
42
|
+
@tasks.delete_if { |t2| t2.to_s == task.to_s }
|
43
|
+
end
|
44
|
+
|
45
|
+
# Return a scheduled task list formatted string.
|
46
|
+
#
|
47
|
+
# @since 0.2
|
48
|
+
def to_s
|
49
|
+
[@header, @tasks.map(&:to_s), nil].flatten.join("\n")
|
50
|
+
end
|
51
|
+
|
52
|
+
def save(path)
|
53
|
+
data = self.to_s
|
54
|
+
File.open(path, 'w:utf-8') do |file|
|
55
|
+
file.puts(data)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# TODO: Next Monday
|
60
|
+
# NOTE: For parsing we don't use %-d etc, only %d.
|
61
|
+
DATE_FORMATS = {
|
62
|
+
'%d/%m' => :next_month, # 1/1
|
63
|
+
'%d/%m/%Y' => nil, # 1/1/2018
|
64
|
+
'%A %d/%m' => :next_week, # Monday 1/1 Note: Higher specifity has to come first.
|
65
|
+
'%A' => :next_week # Monday
|
66
|
+
}
|
67
|
+
|
68
|
+
# labels = ['Tomorrow', date.strftime('%A'), date.strftime('%-d/%m'), date.strftime('%-d/%m/%Y')]
|
69
|
+
def scheduled_date
|
70
|
+
return Date.today if @header == 'Today' # Change tomorrow to Today if you're generating it in the morning.
|
71
|
+
return Date.today + 1 if @header == 'Tomorrow'
|
72
|
+
parse_date_in_the_future(@header)
|
73
|
+
end
|
74
|
+
|
75
|
+
def tomorrow?
|
76
|
+
self.scheduled_date == Date.today + 1
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
# TODO: We need base_date, Date.today wouldn't cut it if we run "now g +3".
|
82
|
+
def parse_date_in_the_future(header)
|
83
|
+
DATE_FORMATS.each do |format, adjustment_method|
|
84
|
+
begin
|
85
|
+
date = Date.strptime(header, format)
|
86
|
+
date.define_singleton_method(:next_week) { self + 7 } # TODO: DataExts, extract it from commands.rb.
|
87
|
+
return ensure_in_the_future(date, adjustment_method)
|
88
|
+
rescue ArgumentError
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
return nil
|
93
|
+
end
|
94
|
+
|
95
|
+
def ensure_in_the_future(date, adjustment_method)
|
96
|
+
return date if adjustment_method.nil? || Date.today <= date
|
97
|
+
date.send(adjustment_method)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
export { TaskGroup }
|
@@ -0,0 +1,106 @@
|
|
1
|
+
class TaskList
|
2
|
+
include Enumerable
|
3
|
+
|
4
|
+
# List of {TaskGroup task groups}. Or more precisely objects responding to `#header` and `#tasks`.
|
5
|
+
# @since 0.2
|
6
|
+
attr_reader :data
|
7
|
+
|
8
|
+
# @param [Array<TaskGroup>] data List of task groups.
|
9
|
+
# Or more precisely objects responding to `#header` and `#tasks`.
|
10
|
+
# @raise [ArgumentError] if data is not an array or if its content doesn't
|
11
|
+
# respond to `#header` and `#tasks`.
|
12
|
+
#
|
13
|
+
# @example
|
14
|
+
# TaskGroup, TaskList = import('scheduled-format').grab(:TaskGroup, :TaskList)
|
15
|
+
#
|
16
|
+
# tasks = ['Buy milk. #errands', '[9:20] Call with Mike.']
|
17
|
+
# group = TaskGroup.new(header: 'Tomorrow', tasks: tasks)
|
18
|
+
# list = TaskList.new([group])
|
19
|
+
# @since 0.2
|
20
|
+
def initialize(data)
|
21
|
+
@data = data
|
22
|
+
|
23
|
+
unless data.is_a?(Array) && data.all? { |item| item.respond_to?(:header) && item.respond_to?(:tasks) }
|
24
|
+
raise ArgumentError.new("Data is supposed to be an array of TaskGroup instances.")
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# Find a task group that matches given header.
|
29
|
+
#
|
30
|
+
# @return [TaskGroup, nil] matching the header.
|
31
|
+
# @since 0.2
|
32
|
+
#
|
33
|
+
# @example
|
34
|
+
# # Using the code from the initialiser.
|
35
|
+
# list['Tomorrow']
|
36
|
+
def [](header)
|
37
|
+
@data.find do |task_group|
|
38
|
+
task_group.header == header
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Add a task group onto the task list.
|
43
|
+
#
|
44
|
+
# @raise [ArgumentError] if the task group is already in the list.
|
45
|
+
# @param [TaskGroup] task_group the task group.
|
46
|
+
# @since 0.2
|
47
|
+
def <<(task_group)
|
48
|
+
unless task_group.is_a?(TaskGroup)
|
49
|
+
raise ArgumentError.new("TaskGroup expected, got #{task_group.class}.")
|
50
|
+
end
|
51
|
+
|
52
|
+
if self[task_group.header]
|
53
|
+
raise ArgumentError.new("Task group with header #{task_group.header} is already on the list.")
|
54
|
+
end
|
55
|
+
|
56
|
+
@data << task_group
|
57
|
+
self.sort!
|
58
|
+
end
|
59
|
+
|
60
|
+
def scheduled_task_groups
|
61
|
+
@data.group_by { |tg| tg.scheduled_date.nil? }[false] || Array.new
|
62
|
+
end
|
63
|
+
|
64
|
+
def non_scheduled_task_groups
|
65
|
+
@data.group_by { |tg| tg.scheduled_date.nil? }[true] || Array.new
|
66
|
+
end
|
67
|
+
|
68
|
+
def sort!
|
69
|
+
@data = self.scheduled_task_groups.sort_by(&:scheduled_date) +
|
70
|
+
self.non_scheduled_task_groups
|
71
|
+
|
72
|
+
self
|
73
|
+
end
|
74
|
+
|
75
|
+
# Remove a task group from the task list.
|
76
|
+
#
|
77
|
+
# @param [TaskGroup] task_group the task group.
|
78
|
+
# @since 0.2
|
79
|
+
def delete(task_group)
|
80
|
+
@data.delete(task_group)
|
81
|
+
end
|
82
|
+
|
83
|
+
# Iterate over the task groups.
|
84
|
+
#
|
85
|
+
# @yieldparam [TaskGroup] task_group
|
86
|
+
# @since 0.2
|
87
|
+
def each(&block)
|
88
|
+
@data.each(&block)
|
89
|
+
end
|
90
|
+
|
91
|
+
# Return a scheduled task list formatted string.
|
92
|
+
#
|
93
|
+
# @since 0.2
|
94
|
+
def to_s
|
95
|
+
@data.map(&:to_s).join("\n")
|
96
|
+
end
|
97
|
+
|
98
|
+
def save(path)
|
99
|
+
data = self.to_s
|
100
|
+
File.open(path, 'w:utf-8') do |file|
|
101
|
+
file.puts(data)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
export { TaskList }
|
metadata
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: scheduled-format
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- James C Russell
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2018-06-07 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: commonjs_modules
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: parslet
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.8'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.8'
|
41
|
+
description: "."
|
42
|
+
email: james@101ideas.cz
|
43
|
+
executables: []
|
44
|
+
extensions: []
|
45
|
+
extra_rdoc_files: []
|
46
|
+
files:
|
47
|
+
- README.md
|
48
|
+
- lib/scheduled-format.rb
|
49
|
+
- lib/scheduled-format/parser/parser.rb
|
50
|
+
- lib/scheduled-format/parser/transformer.rb
|
51
|
+
- lib/scheduled-format/task.rb
|
52
|
+
- lib/scheduled-format/task_group.rb
|
53
|
+
- lib/scheduled-format/task_list.rb
|
54
|
+
homepage: http://github.com/botanicus/scheduled-format
|
55
|
+
licenses:
|
56
|
+
- MIT
|
57
|
+
metadata: {}
|
58
|
+
post_install_message:
|
59
|
+
rdoc_options: []
|
60
|
+
require_paths:
|
61
|
+
- lib
|
62
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
63
|
+
requirements:
|
64
|
+
- - ">="
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
version: '0'
|
67
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
68
|
+
requirements:
|
69
|
+
- - ">="
|
70
|
+
- !ruby/object:Gem::Version
|
71
|
+
version: '0'
|
72
|
+
requirements: []
|
73
|
+
rubyforge_project:
|
74
|
+
rubygems_version: 2.7.6
|
75
|
+
signing_key:
|
76
|
+
specification_version: 4
|
77
|
+
summary: ''
|
78
|
+
test_files: []
|