standup_md 1.0.0 → 2.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 +4 -4
- data/.github/workflows/ruby.yml +4 -6
- data/Gemfile.lock +2 -2
- data/README.md +194 -50
- data/completion/zsh/_standup +12 -17
- data/lib/standup_md/cli/helpers.rb +63 -61
- data/lib/standup_md/cli.rb +44 -22
- data/lib/standup_md/config/cli.rb +70 -7
- data/lib/standup_md/config/entry.rb +31 -1
- data/lib/standup_md/config/file.rb +41 -7
- data/lib/standup_md/config/post.rb +152 -0
- data/lib/standup_md/config.rb +18 -5
- data/lib/standup_md/entry.rb +31 -18
- data/lib/standup_md/entry_list.rb +5 -22
- data/lib/standup_md/file.rb +57 -54
- data/lib/standup_md/parsers/markdown.rb +44 -34
- data/lib/standup_md/post/adapter.rb +41 -0
- data/lib/standup_md/post/adapters/slack.rb +108 -0
- data/lib/standup_md/post/message.rb +47 -0
- data/lib/standup_md/post/result.rb +87 -0
- data/lib/standup_md/post.rb +84 -0
- data/lib/standup_md/section.rb +2 -13
- data/lib/standup_md/task.rb +0 -9
- data/lib/standup_md/version.rb +5 -1
- data/lib/standup_md.rb +2 -2
- data/standup_md.gemspec +1 -0
- metadata +8 -4
- data/lib/standup_md/config/entry_list.rb +0 -29
- data/lib/standup_md/title.rb +0 -41
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "standup_md/post"
|
|
4
|
+
|
|
5
|
+
module StandupMD
|
|
6
|
+
class Config
|
|
7
|
+
##
|
|
8
|
+
# The configuration class for chat posting.
|
|
9
|
+
class Post
|
|
10
|
+
##
|
|
11
|
+
# The default posting options.
|
|
12
|
+
#
|
|
13
|
+
# @return [Hash]
|
|
14
|
+
DEFAULTS = {
|
|
15
|
+
default_adapter: :slack,
|
|
16
|
+
title: nil
|
|
17
|
+
}.freeze
|
|
18
|
+
|
|
19
|
+
##
|
|
20
|
+
# Attributes copied into request-scoped config snapshots.
|
|
21
|
+
#
|
|
22
|
+
# @return [Array<Symbol>]
|
|
23
|
+
CONFIG_ATTRIBUTES = DEFAULTS.keys.freeze
|
|
24
|
+
|
|
25
|
+
##
|
|
26
|
+
# The adapter used when `standup --post` is called without a platform.
|
|
27
|
+
#
|
|
28
|
+
# @return [Symbol]
|
|
29
|
+
attr_accessor :default_adapter
|
|
30
|
+
|
|
31
|
+
##
|
|
32
|
+
# Format string for posted entry titles.
|
|
33
|
+
#
|
|
34
|
+
# This only affects messages sent through chat posting adapters. It does
|
|
35
|
+
# not change stored standup markdown files. Use `%s` as a placeholder for
|
|
36
|
+
# the normal entry title, such as the entry date. This is useful when chat
|
|
37
|
+
# clients post through workspace apps or bots with shared names like
|
|
38
|
+
# "StandupMD", but the message should still identify whose standup it is.
|
|
39
|
+
#
|
|
40
|
+
# @example Include the person's name after the entry date
|
|
41
|
+
# StandupMD.config.post.title = "%s - Evan Gray"
|
|
42
|
+
#
|
|
43
|
+
# @return [String, nil]
|
|
44
|
+
attr_accessor :title
|
|
45
|
+
|
|
46
|
+
##
|
|
47
|
+
# Registered adapter classes or instances.
|
|
48
|
+
#
|
|
49
|
+
# @return [Hash]
|
|
50
|
+
attr_reader :adapters
|
|
51
|
+
|
|
52
|
+
##
|
|
53
|
+
# Non-secret adapter options.
|
|
54
|
+
#
|
|
55
|
+
# @return [Hash]
|
|
56
|
+
attr_reader :adapter_options
|
|
57
|
+
|
|
58
|
+
##
|
|
59
|
+
# Initializes the config with default values.
|
|
60
|
+
def initialize
|
|
61
|
+
reset
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
##
|
|
65
|
+
# Sets all config values back to their defaults.
|
|
66
|
+
#
|
|
67
|
+
# @return [Hash]
|
|
68
|
+
def reset
|
|
69
|
+
DEFAULTS.each { |k, v| instance_variable_set("@#{k}", v) }
|
|
70
|
+
@adapters = {}
|
|
71
|
+
@adapter_options = Hash.new { |hash, key| hash[key] = {} }
|
|
72
|
+
register_adapter(:slack, StandupMD::Post::Adapters::Slack)
|
|
73
|
+
DEFAULTS
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
##
|
|
77
|
+
# Copies values from another post config.
|
|
78
|
+
#
|
|
79
|
+
# @param [StandupMD::Config::Post] config
|
|
80
|
+
#
|
|
81
|
+
# @return [StandupMD::Config::Post]
|
|
82
|
+
def copy_from(config)
|
|
83
|
+
CONFIG_ATTRIBUTES.each do |attribute|
|
|
84
|
+
instance_variable_set("@#{attribute}", config.public_send(attribute))
|
|
85
|
+
end
|
|
86
|
+
@adapters = config.adapters.dup
|
|
87
|
+
@adapter_options = Hash.new { |hash, key| hash[key] = {} }
|
|
88
|
+
config.adapter_options.each do |name, options|
|
|
89
|
+
@adapter_options[name] = options.dup
|
|
90
|
+
end
|
|
91
|
+
self
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
##
|
|
95
|
+
# Registers a posting adapter.
|
|
96
|
+
#
|
|
97
|
+
# @param name [String, Symbol]
|
|
98
|
+
# @param adapter [Class, Object]
|
|
99
|
+
#
|
|
100
|
+
# @return [Class, Object]
|
|
101
|
+
def register_adapter(name, adapter)
|
|
102
|
+
adapters[name.to_sym] = adapter
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
##
|
|
106
|
+
# Configures non-secret adapter options.
|
|
107
|
+
#
|
|
108
|
+
# @param name [String, Symbol]
|
|
109
|
+
# @param options [Hash]
|
|
110
|
+
#
|
|
111
|
+
# @return [Hash]
|
|
112
|
+
def configure_adapter(name, options = {})
|
|
113
|
+
options_for(name).merge!(symbolize_keys(options))
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
##
|
|
117
|
+
# Returns non-secret options for an adapter.
|
|
118
|
+
#
|
|
119
|
+
# @param name [String, Symbol]
|
|
120
|
+
#
|
|
121
|
+
# @return [Hash]
|
|
122
|
+
def options_for(name)
|
|
123
|
+
adapter_options[name.to_sym]
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
##
|
|
127
|
+
# Builds the adapter requested by name.
|
|
128
|
+
#
|
|
129
|
+
# @param name [String, Symbol, nil]
|
|
130
|
+
#
|
|
131
|
+
# @return [Object]
|
|
132
|
+
def build_adapter(name = nil)
|
|
133
|
+
adapter_name = (name || default_adapter).to_sym
|
|
134
|
+
adapter = adapters.fetch(adapter_name) do
|
|
135
|
+
raise StandupMD::Post::UnknownAdapter, "No post adapter registered for #{adapter_name}"
|
|
136
|
+
end
|
|
137
|
+
return adapter unless adapter.respond_to?(:new)
|
|
138
|
+
return adapter.new if adapter.instance_method(:initialize).arity.zero?
|
|
139
|
+
|
|
140
|
+
adapter.new(options_for(adapter_name))
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
private
|
|
144
|
+
|
|
145
|
+
def symbolize_keys(hash)
|
|
146
|
+
hash.each_with_object({}) do |(key, value), result|
|
|
147
|
+
result[key.to_sym] = value
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
data/lib/standup_md/config.rb
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
require "standup_md/config/cli"
|
|
4
4
|
require "standup_md/config/file"
|
|
5
5
|
require "standup_md/config/entry"
|
|
6
|
-
require "standup_md/config/
|
|
6
|
+
require "standup_md/config/post"
|
|
7
7
|
|
|
8
8
|
module StandupMD
|
|
9
9
|
##
|
|
@@ -28,10 +28,10 @@ module StandupMD
|
|
|
28
28
|
attr_reader :entry
|
|
29
29
|
|
|
30
30
|
##
|
|
31
|
-
# Reader for
|
|
31
|
+
# Reader for Post config.
|
|
32
32
|
#
|
|
33
|
-
# @return [StandupMD::Config::
|
|
34
|
-
attr_reader :
|
|
33
|
+
# @return [StandupMD::Config::Post]
|
|
34
|
+
attr_reader :post
|
|
35
35
|
|
|
36
36
|
##
|
|
37
37
|
# Builds the links to the configuration classes.
|
|
@@ -39,7 +39,20 @@ module StandupMD
|
|
|
39
39
|
@cli = StandupMD::Config::Cli.new
|
|
40
40
|
@file = StandupMD::Config::File.new
|
|
41
41
|
@entry = StandupMD::Config::Entry.new
|
|
42
|
-
@
|
|
42
|
+
@post = StandupMD::Config::Post.new
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
##
|
|
46
|
+
# Builds an independent snapshot of the current configuration.
|
|
47
|
+
#
|
|
48
|
+
# @return [StandupMD::Config]
|
|
49
|
+
def copy
|
|
50
|
+
self.class.new.tap do |config|
|
|
51
|
+
config.cli.copy_from(cli)
|
|
52
|
+
config.file.copy_from(file)
|
|
53
|
+
config.entry.copy_from(entry)
|
|
54
|
+
config.post.copy_from(post)
|
|
55
|
+
end
|
|
43
56
|
end
|
|
44
57
|
end
|
|
45
58
|
end
|
data/lib/standup_md/entry.rb
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "json"
|
|
4
3
|
require "standup_md/section"
|
|
5
4
|
|
|
6
5
|
module StandupMD
|
|
@@ -10,6 +9,10 @@ module StandupMD
|
|
|
10
9
|
class Entry
|
|
11
10
|
include Comparable
|
|
12
11
|
|
|
12
|
+
##
|
|
13
|
+
# Section identifiers supported by each standup entry.
|
|
14
|
+
#
|
|
15
|
+
# @return [Array<Symbol>]
|
|
13
16
|
SECTION_TYPES = %i[current previous impediments notes].freeze
|
|
14
17
|
|
|
15
18
|
##
|
|
@@ -17,7 +20,7 @@ module StandupMD
|
|
|
17
20
|
#
|
|
18
21
|
# @return [StandupMD::Config::Entry]
|
|
19
22
|
def self.config
|
|
20
|
-
|
|
23
|
+
StandupMD.config.entry
|
|
21
24
|
end
|
|
22
25
|
|
|
23
26
|
##
|
|
@@ -32,14 +35,33 @@ module StandupMD
|
|
|
32
35
|
# Creates a generic entry. Default values can be set via configuration.
|
|
33
36
|
# Yields the entry if a block is passed so you can change values.
|
|
34
37
|
#
|
|
38
|
+
# @param [StandupMD::Config::Entry] config
|
|
39
|
+
#
|
|
40
|
+
# @param [Date] date
|
|
41
|
+
#
|
|
42
|
+
# @param [Array, nil] current
|
|
43
|
+
#
|
|
44
|
+
# @param [Array, nil] previous
|
|
45
|
+
#
|
|
46
|
+
# @param [Array, nil] impediments
|
|
47
|
+
#
|
|
48
|
+
# @param [Array, nil] notes
|
|
49
|
+
#
|
|
35
50
|
# @return [StandupMD::Entry]
|
|
36
|
-
def self.create
|
|
51
|
+
def self.create(
|
|
52
|
+
config: StandupMD.config.entry,
|
|
53
|
+
date: Date.today,
|
|
54
|
+
current: nil,
|
|
55
|
+
previous: nil,
|
|
56
|
+
impediments: nil,
|
|
57
|
+
notes: nil
|
|
58
|
+
)
|
|
37
59
|
new(
|
|
38
|
-
|
|
39
|
-
config.current,
|
|
40
|
-
config.previous,
|
|
41
|
-
config.impediments,
|
|
42
|
-
config.notes
|
|
60
|
+
date,
|
|
61
|
+
current || config.current,
|
|
62
|
+
previous || config.previous,
|
|
63
|
+
impediments || config.impediments,
|
|
64
|
+
notes || config.notes
|
|
43
65
|
).tap { |entry| yield entry if block_given? }
|
|
44
66
|
end
|
|
45
67
|
|
|
@@ -58,7 +80,6 @@ module StandupMD
|
|
|
58
80
|
def initialize(date, current, previous, impediments, notes = [])
|
|
59
81
|
raise unless date.is_a?(Date)
|
|
60
82
|
|
|
61
|
-
@config = self.class.config
|
|
62
83
|
@date = date
|
|
63
84
|
@sections = {}
|
|
64
85
|
self.current = current
|
|
@@ -106,7 +127,7 @@ module StandupMD
|
|
|
106
127
|
end
|
|
107
128
|
|
|
108
129
|
##
|
|
109
|
-
# Entry as a hash
|
|
130
|
+
# Entry as a hash.
|
|
110
131
|
#
|
|
111
132
|
# @return [Hash]
|
|
112
133
|
def to_h
|
|
@@ -120,14 +141,6 @@ module StandupMD
|
|
|
120
141
|
}
|
|
121
142
|
end
|
|
122
143
|
|
|
123
|
-
##
|
|
124
|
-
# Entry as a json object.
|
|
125
|
-
#
|
|
126
|
-
# @return [String]
|
|
127
|
-
def to_json
|
|
128
|
-
to_h.to_json
|
|
129
|
-
end
|
|
130
|
-
|
|
131
144
|
private
|
|
132
145
|
|
|
133
146
|
def set_section(type, tasks)
|
|
@@ -10,15 +10,7 @@ module StandupMD
|
|
|
10
10
|
include Enumerable
|
|
11
11
|
|
|
12
12
|
##
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
# @return [StandupMD::Config::EntryList]
|
|
16
|
-
def self.config
|
|
17
|
-
@config ||= StandupMD.config.entry_list
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
##
|
|
21
|
-
# Contruct a list. Can pass any amount of +StandupMD::Entry+ instances.
|
|
13
|
+
# Construct a list. Can pass any amount of +StandupMD::Entry+ instances.
|
|
22
14
|
#
|
|
23
15
|
# @param [Entry] entries
|
|
24
16
|
#
|
|
@@ -26,7 +18,6 @@ module StandupMD
|
|
|
26
18
|
def initialize(*entries)
|
|
27
19
|
entries.each { |entry| validate_entry(entry) }
|
|
28
20
|
|
|
29
|
-
@config = self.class.config
|
|
30
21
|
@entries = entries
|
|
31
22
|
end
|
|
32
23
|
|
|
@@ -35,7 +26,7 @@ module StandupMD
|
|
|
35
26
|
#
|
|
36
27
|
# @param [StandupMD::Entry] entry
|
|
37
28
|
#
|
|
38
|
-
# @return [
|
|
29
|
+
# @return [StandupMD::EntryList]
|
|
39
30
|
def <<(entry)
|
|
40
31
|
validate_entry(entry)
|
|
41
32
|
|
|
@@ -100,9 +91,9 @@ module StandupMD
|
|
|
100
91
|
#
|
|
101
92
|
# @param [Date] end_date
|
|
102
93
|
#
|
|
103
|
-
# @return [
|
|
94
|
+
# @return [StandupMD::EntryList]
|
|
104
95
|
def filter!(start_date, end_date)
|
|
105
|
-
@entries = filter(start_date, end_date)
|
|
96
|
+
@entries = filter(start_date, end_date).to_a
|
|
106
97
|
self
|
|
107
98
|
end
|
|
108
99
|
|
|
@@ -124,18 +115,10 @@ module StandupMD
|
|
|
124
115
|
end.to_h
|
|
125
116
|
end
|
|
126
117
|
|
|
127
|
-
##
|
|
128
|
-
# The entry list as a json object.
|
|
129
|
-
#
|
|
130
|
-
# @return [String]
|
|
131
|
-
def to_json
|
|
132
|
-
to_h.to_json
|
|
133
|
-
end
|
|
134
|
-
|
|
135
118
|
# :section: Delegators
|
|
136
119
|
|
|
137
120
|
##
|
|
138
|
-
# The following are forwarded to @entries, which is the
|
|
121
|
+
# The following are forwarded to @entries, which is the underlying array of
|
|
139
122
|
# entries.
|
|
140
123
|
#
|
|
141
124
|
# +each+:: Iterate over each entry.
|
data/lib/standup_md/file.rb
CHANGED
|
@@ -8,68 +8,59 @@ module StandupMD
|
|
|
8
8
|
##
|
|
9
9
|
# Class for handling reading and writing standup files.
|
|
10
10
|
class File
|
|
11
|
+
##
|
|
12
|
+
# Raised when a standup file or directory is missing and creation is off.
|
|
13
|
+
class NotFoundError < StandardError; end
|
|
14
|
+
|
|
11
15
|
class << self
|
|
12
16
|
##
|
|
13
17
|
# Access to the class's configuration.
|
|
14
18
|
#
|
|
15
|
-
# @return [StandupMD::Config::
|
|
19
|
+
# @return [StandupMD::Config::File]
|
|
16
20
|
def config
|
|
17
|
-
|
|
21
|
+
StandupMD.config.file
|
|
18
22
|
end
|
|
19
23
|
|
|
20
24
|
##
|
|
21
|
-
# Convenience method for calling File.find(file_name).load
|
|
25
|
+
# Convenience method for calling File.find(file_name).load.
|
|
22
26
|
#
|
|
23
27
|
# @param [String] file_name
|
|
28
|
+
# @param [StandupMD::Config::File] config
|
|
24
29
|
#
|
|
25
30
|
# @return [StandupMD::File]
|
|
26
|
-
def load(file_name)
|
|
27
|
-
|
|
28
|
-
raise "Dir #{config.directory} not found." unless config.create
|
|
29
|
-
|
|
30
|
-
FileUtils.mkdir_p(config.directory)
|
|
31
|
-
end
|
|
32
|
-
new(file_name).load
|
|
31
|
+
def load(file_name, config: StandupMD.config.file)
|
|
32
|
+
new(file_name, config: config).load
|
|
33
33
|
end
|
|
34
34
|
|
|
35
35
|
##
|
|
36
36
|
# Find standup file in directory by file name.
|
|
37
37
|
#
|
|
38
|
-
# @param [String]
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
end
|
|
45
|
-
file_path = ::File.join(config.directory, file_name)
|
|
46
|
-
unless ::File.file?(file_path) || config.create
|
|
47
|
-
raise "File #{file_name} not found."
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
new(file_name)
|
|
38
|
+
# @param [String] file_name
|
|
39
|
+
# @param [StandupMD::Config::File] config
|
|
40
|
+
#
|
|
41
|
+
# @return [StandupMD::File]
|
|
42
|
+
def find(file_name, config: StandupMD.config.file)
|
|
43
|
+
new(file_name, config: config)
|
|
51
44
|
end
|
|
52
45
|
|
|
53
46
|
##
|
|
54
47
|
# Find standup file in directory by Date object.
|
|
55
48
|
#
|
|
56
49
|
# @param [Date] date
|
|
57
|
-
|
|
50
|
+
# @param [StandupMD::Config::File] config
|
|
51
|
+
#
|
|
52
|
+
# @return [StandupMD::File]
|
|
53
|
+
def find_by_date(date, config: StandupMD.config.file)
|
|
58
54
|
raise ArgumentError, "Must be a Date object" unless date.is_a?(Date)
|
|
59
55
|
|
|
60
|
-
|
|
61
|
-
raise "Dir #{config.directory} not found." unless config.create
|
|
62
|
-
|
|
63
|
-
FileUtils.mkdir_p(config.directory)
|
|
64
|
-
end
|
|
65
|
-
find(date.strftime(config.name_format))
|
|
56
|
+
find(date.strftime(config.name_format), config: config)
|
|
66
57
|
end
|
|
67
58
|
end
|
|
68
59
|
|
|
69
60
|
##
|
|
70
61
|
# The list of entries in the file.
|
|
71
62
|
#
|
|
72
|
-
# @return [
|
|
63
|
+
# @return [StandupMD::EntryList]
|
|
73
64
|
attr_reader :entries
|
|
74
65
|
|
|
75
66
|
##
|
|
@@ -82,29 +73,20 @@ module StandupMD
|
|
|
82
73
|
# Constructs the instance.
|
|
83
74
|
#
|
|
84
75
|
# @param [String] file_name
|
|
76
|
+
# @param [StandupMD::Config::File] config
|
|
85
77
|
#
|
|
86
|
-
# @return [
|
|
87
|
-
def initialize(file_name)
|
|
88
|
-
@config =
|
|
78
|
+
# @return [StandupMD::File]
|
|
79
|
+
def initialize(file_name, config: StandupMD.config.file)
|
|
80
|
+
@config = config
|
|
89
81
|
@parser = StandupMD::Parsers::Markdown.new(@config)
|
|
90
82
|
if file_name.include?(::File::SEPARATOR)
|
|
91
83
|
raise ArgumentError,
|
|
92
|
-
"#{file_name} contains directory.
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
unless ::File.directory?(@config.directory)
|
|
96
|
-
raise "Dir #{@config.directory} not found." unless @config.create
|
|
97
|
-
|
|
98
|
-
FileUtils.mkdir_p(@config.directory)
|
|
84
|
+
"#{file_name} contains directory. Configure the file directory separately."
|
|
99
85
|
end
|
|
100
86
|
|
|
87
|
+
ensure_directory
|
|
101
88
|
@name = ::File.expand_path(::File.join(@config.directory, file_name))
|
|
102
|
-
|
|
103
|
-
unless ::File.file?(@name)
|
|
104
|
-
raise "File #{@name} not found." unless @config.create
|
|
105
|
-
|
|
106
|
-
FileUtils.touch(@name)
|
|
107
|
-
end
|
|
89
|
+
ensure_file
|
|
108
90
|
|
|
109
91
|
@new = ::File.zero?(@name)
|
|
110
92
|
@loaded = false
|
|
@@ -137,28 +119,49 @@ module StandupMD
|
|
|
137
119
|
##
|
|
138
120
|
# Loads the file's contents.
|
|
139
121
|
#
|
|
140
|
-
# @return [StandupMD::
|
|
122
|
+
# @return [StandupMD::File]
|
|
141
123
|
def load
|
|
142
|
-
raise "File #{name} does not exist." unless ::File.file?(name)
|
|
124
|
+
raise NotFoundError, "File #{name} does not exist." unless ::File.file?(name)
|
|
143
125
|
|
|
144
126
|
@loaded = true
|
|
145
|
-
@entries = @parser.read(name)
|
|
127
|
+
@entries = @parser.parse(::File.read(name))
|
|
146
128
|
self
|
|
147
129
|
end
|
|
148
130
|
|
|
149
131
|
##
|
|
150
|
-
# Writes
|
|
151
|
-
#
|
|
152
|
-
# already exists, it will be clobbered with the entries in the range.
|
|
132
|
+
# Writes entries to disk. This method is destructive; existing file contents
|
|
133
|
+
# are replaced by the rendered entries in the requested date range.
|
|
153
134
|
#
|
|
154
135
|
# @param [Hash] {start_date: Date, end_date: Date}
|
|
155
136
|
#
|
|
156
137
|
# @return [Boolean] true if successful
|
|
157
138
|
def write(**dates)
|
|
139
|
+
raise ArgumentError, "No entries loaded for #{name}" if entries.nil? || entries.empty?
|
|
140
|
+
|
|
158
141
|
sorted_entries = entries.sort
|
|
159
142
|
start_date = dates.fetch(:start_date, sorted_entries.first.date)
|
|
160
143
|
end_date = dates.fetch(:end_date, sorted_entries.last.date)
|
|
161
|
-
|
|
144
|
+
::File.write(
|
|
145
|
+
name,
|
|
146
|
+
@parser.render(sorted_entries, start_date: start_date, end_date: end_date)
|
|
147
|
+
)
|
|
148
|
+
true
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
private
|
|
152
|
+
|
|
153
|
+
def ensure_directory
|
|
154
|
+
return if ::File.directory?(@config.directory)
|
|
155
|
+
raise NotFoundError, "Dir #{@config.directory} not found." unless @config.create
|
|
156
|
+
|
|
157
|
+
FileUtils.mkdir_p(@config.directory)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def ensure_file
|
|
161
|
+
return if ::File.file?(@name)
|
|
162
|
+
raise NotFoundError, "File #{@name} not found." unless @config.create
|
|
163
|
+
|
|
164
|
+
FileUtils.touch(@name)
|
|
162
165
|
end
|
|
163
166
|
end
|
|
164
167
|
end
|
|
@@ -5,13 +5,18 @@ require "standup_md/entry"
|
|
|
5
5
|
require "standup_md/entry_list"
|
|
6
6
|
require "standup_md/section"
|
|
7
7
|
require "standup_md/task"
|
|
8
|
-
require "standup_md/title"
|
|
9
8
|
|
|
10
9
|
module StandupMD
|
|
10
|
+
##
|
|
11
|
+
# Namespace for standup file parsers and renderers.
|
|
11
12
|
module Parsers
|
|
12
13
|
##
|
|
13
14
|
# Parser and renderer for the markdown standup format.
|
|
14
15
|
class Markdown
|
|
16
|
+
##
|
|
17
|
+
# Raised when markdown cannot be parsed into standup entries.
|
|
18
|
+
class Error < StandardError; end
|
|
19
|
+
|
|
15
20
|
##
|
|
16
21
|
# Access to file configuration.
|
|
17
22
|
#
|
|
@@ -27,17 +32,17 @@ module StandupMD
|
|
|
27
32
|
end
|
|
28
33
|
|
|
29
34
|
##
|
|
30
|
-
#
|
|
35
|
+
# Parses entries from markdown text.
|
|
31
36
|
#
|
|
32
|
-
# @param [String]
|
|
37
|
+
# @param [String] text
|
|
33
38
|
#
|
|
34
39
|
# @return [StandupMD::EntryList]
|
|
35
|
-
def
|
|
40
|
+
def parse(text)
|
|
36
41
|
entry_list = EntryList.new
|
|
37
42
|
record = nil
|
|
38
43
|
section = nil
|
|
39
44
|
|
|
40
|
-
|
|
45
|
+
text.to_s.each_line do |line|
|
|
41
46
|
line.chomp!
|
|
42
47
|
next if line.strip.empty?
|
|
43
48
|
|
|
@@ -57,42 +62,40 @@ module StandupMD
|
|
|
57
62
|
entry_list << entry(record) if record
|
|
58
63
|
entry_list.sort
|
|
59
64
|
rescue => e
|
|
60
|
-
raise "
|
|
65
|
+
raise Error, "Markdown malformation: #{e.message}"
|
|
61
66
|
end
|
|
62
67
|
|
|
63
68
|
##
|
|
64
|
-
#
|
|
69
|
+
# Renders entries as markdown text.
|
|
65
70
|
#
|
|
66
|
-
# @param [String] file_name
|
|
67
71
|
# @param [StandupMD::EntryList] entries
|
|
68
72
|
# @param [Date] start_date
|
|
69
73
|
# @param [Date] end_date
|
|
70
74
|
#
|
|
71
|
-
# @return [
|
|
72
|
-
def
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
config.sub_header_order.each do |attr|
|
|
77
|
-
section = Section.new(attr, entry.public_send("#{attr}_tasks"))
|
|
78
|
-
next if section.empty?
|
|
79
|
-
|
|
80
|
-
f.puts section.to_markdown
|
|
81
|
-
end
|
|
82
|
-
f.puts
|
|
83
|
-
end
|
|
84
|
-
end
|
|
85
|
-
true
|
|
75
|
+
# @return [String]
|
|
76
|
+
def render(entries, start_date:, end_date:)
|
|
77
|
+
entries.filter(start_date, end_date).sort_reverse.map do |entry|
|
|
78
|
+
render_entry(entry)
|
|
79
|
+
end.join
|
|
86
80
|
end
|
|
87
81
|
|
|
88
82
|
##
|
|
89
|
-
# Renders a
|
|
83
|
+
# Renders a single entry as markdown text.
|
|
90
84
|
#
|
|
91
|
-
# @param [
|
|
85
|
+
# @param [StandupMD::Entry] entry
|
|
92
86
|
#
|
|
93
87
|
# @return [String]
|
|
94
|
-
def
|
|
95
|
-
|
|
88
|
+
def render_entry(entry)
|
|
89
|
+
lines = [entry_header(entry)]
|
|
90
|
+
config.sub_header_order.each do |type|
|
|
91
|
+
section = Section.new(type, entry.public_send("#{type}_tasks"))
|
|
92
|
+
next if section.empty?
|
|
93
|
+
|
|
94
|
+
lines << section_header(type)
|
|
95
|
+
section.tasks.each { |task| lines << task_line(task) }
|
|
96
|
+
end
|
|
97
|
+
lines << ""
|
|
98
|
+
lines.join("\n") + "\n"
|
|
96
99
|
end
|
|
97
100
|
|
|
98
101
|
private
|
|
@@ -121,11 +124,19 @@ module StandupMD
|
|
|
121
124
|
Section.new(type)
|
|
122
125
|
end
|
|
123
126
|
|
|
127
|
+
def entry_header(entry)
|
|
128
|
+
"#{"#" * config.header_depth} #{entry.date.strftime(config.header_date_format)}"
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def section_header(type)
|
|
132
|
+
"#{"#" * config.sub_header_depth} #{config.public_send("#{type}_header")}"
|
|
133
|
+
end
|
|
134
|
+
|
|
124
135
|
def section_type(line)
|
|
125
136
|
sub_header = line.sub(/^\#{#{config.sub_header_depth}}\s*/, "")
|
|
126
137
|
Entry::SECTION_TYPES.each do |type|
|
|
127
138
|
header = config.public_send("#{type}_header")
|
|
128
|
-
return type if sub_header
|
|
139
|
+
return type if sub_header == header
|
|
129
140
|
end
|
|
130
141
|
raise "Unrecognized header [#{sub_header}]"
|
|
131
142
|
end
|
|
@@ -144,6 +155,11 @@ module StandupMD
|
|
|
144
155
|
/\A(?<indent>\s*)#{Regexp.escape(config.bullet_character)}\s*(?<text>.*)\z/
|
|
145
156
|
end
|
|
146
157
|
|
|
158
|
+
def task_line(task)
|
|
159
|
+
indent = " " * config.indent_width * task.indent_level
|
|
160
|
+
"#{indent}#{config.bullet_character} #{task.text}"
|
|
161
|
+
end
|
|
162
|
+
|
|
147
163
|
def entry(record)
|
|
148
164
|
Entry.new(
|
|
149
165
|
Date.strptime(record[:title], config.header_date_format),
|
|
@@ -157,12 +173,6 @@ module StandupMD
|
|
|
157
173
|
def tasks(record, type)
|
|
158
174
|
record[:sections].fetch(type, Section.new(type)).tasks
|
|
159
175
|
end
|
|
160
|
-
|
|
161
|
-
def build_task(task)
|
|
162
|
-
return task if task.is_a?(Task)
|
|
163
|
-
|
|
164
|
-
Task.new(task)
|
|
165
|
-
end
|
|
166
176
|
end
|
|
167
177
|
end
|
|
168
178
|
end
|