standup_md 0.3.16 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 171d0b1fdd697049f52d50db4d1b0ccb95db648dc70b67957c612ebdf74c57b2
4
- data.tar.gz: 1c2347af65439e1d8263715eeb3d9b45b4cbc020793ddb1a51dbbd71ee71159b
3
+ metadata.gz: 2734d31b2ce72d31aceeb789f562b015c4f0e7422b3a755992df7c1f293a7051
4
+ data.tar.gz: 651d1f6f9a613366b90e2b26ba83b8e05dd80dbc5d8bde3a0fcc7d07b57acef1
5
5
  SHA512:
6
- metadata.gz: faca92ff7d839b8fb83c579bc203ba3477e975d225f2fd7ef5c3c8b37625a674ed6a7e89e7f9033c2169f5cc6f52c0e7ae9a046fb29880bd95f1ac7a56bc400c
7
- data.tar.gz: f365e31e43288e039e9c35b887f905a5def1233200d34fead1a250df79215f107cc5ff16534e503d074f1e2b29620fad64b285f94ce68042226cc3a6a1e04fcf
6
+ metadata.gz: 5df3037a20d107f82cc34533f89ab8c31a2ba13eb8180b5931b052c500c5c1f7c4922d22ffed254c5fb9e9d7623b5bb207ed5d5c0c22773bcc972a91bba061d4
7
+ data.tar.gz: c99e5cf930d4dcf84e6089d458321b00f427ab0c124ecf3b02b8e98d79ba416d5c629793f6f77a8cf1a0337b05ed620600833f1ad8fa2775d7e6eb7205ec22ed
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- standup_md (0.3.16)
4
+ standup_md (1.0.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
@@ -115,7 +115,7 @@ CHECKSUMS
115
115
  standard (1.54.0) sha256=7a4b08f83d9893083c8f03bc486f0feeb6a84d48233b40829c03ef4767ea0100
116
116
  standard-custom (1.0.2) sha256=424adc84179a074f1a2a309bb9cf7cd6bfdb2b6541f20c6bf9436c0ba22a652b
117
117
  standard-performance (1.9.0) sha256=49483d31be448292951d80e5e67cdcb576c2502103c7b40aec6f1b6e9c88e3f2
118
- standup_md (0.3.16)
118
+ standup_md (1.0.0)
119
119
  stringio (3.2.0) sha256=c37cb2e58b4ffbd33fe5cd948c05934af997b36e0b6ca6fdf43afa234cf222e1
120
120
  test-unit (3.7.8) sha256=689d1ca53f4d46f678b4e5d68d8e4294f87fc5e8b9238cc4de7c5727e8d65943
121
121
  tsort (0.2.0) sha256=9650a793f6859a43b6641671278f79cfead60ac714148aabe4e3f0060480089f
data/README.md CHANGED
@@ -59,6 +59,17 @@ cd standup_md
59
59
  rake install
60
60
  ```
61
61
 
62
+ ### Zsh Completion
63
+ The gem ships with a zsh completion file at `completion/zsh/_standup`. RubyGems
64
+ doesn't automatically add gem-provided completion directories to zsh's `fpath`,
65
+ so you'll need to add the completion directory yourself after installation.
66
+
67
+ For setup instructions and example, run the following.
68
+
69
+ ```sh
70
+ standup --zsh-completion
71
+ ```
72
+
62
73
  ## Usage
63
74
  ### Command Line
64
75
  For the most basic usage, simply call the executable.
@@ -169,6 +180,7 @@ StandupMD.configure do |c|
169
180
  c.file.sub_header_order = %w[previous current impediments notes]
170
181
  c.file.directory = ::File.join(ENV["HOME"], ".cache", "standup_md")
171
182
  c.file.bullet_character = "-"
183
+ c.file.indent_width = 2
172
184
  c.file.name_format = "%Y_%m.md"
173
185
  c.file.create = true
174
186
 
@@ -207,6 +219,7 @@ Some of these options can be changed at runtime. They are as follows.
207
219
  --impediments ARRAY List of impediments for current entry
208
220
  --notes ARRAY List of notes for current entry
209
221
  --sub-header-order ARRAY The order of the sub-headers when writing the file
222
+ --indent-width INTEGER Number of spaces used for each nested task level
210
223
  -f, --file-name-format STRING Date-formattable string to use for standup file name
211
224
  -E, --editor EDITOR Editor to use for opening standup files
212
225
  -d, --directory DIRECTORY The directories where standup files are located
@@ -214,6 +227,7 @@ Some of these options can be changed at runtime. They are as follows.
214
227
  -a --[no-]auto-fill-previous Auto-generate 'previous' tasks for new entries
215
228
  -e --[no-]edit Open the file in the editor. Default is true
216
229
  -v, --[no-]verbose Verbose output. Default is false.
230
+ --zsh-completion Print zsh completion setup instructions
217
231
  -p, --print [DATE] Print current entry.
218
232
  If DATE is passed, will print entry for DATE, if it exists.
219
233
  DATE must be in the same format as file-name-format
@@ -263,6 +277,7 @@ StandupMD.configure do |c|
263
277
  c.file.previous_header = "Yesterday"
264
278
  c.file.impediments_header = "Hold-ups"
265
279
  c.file.bullet_character = "*"
280
+ c.file.indent_width = 2
266
281
  c.file.header_date_format = "%m/%d/%Y"
267
282
  c.file.sub_header_order = %w[current previous impediments notes]
268
283
  end
@@ -0,0 +1,81 @@
1
+ #compdef standup
2
+
3
+ _standup_array_values() {
4
+ _message -e values 'comma-separated list'
5
+ }
6
+
7
+ _standup_sub_header_order() {
8
+ local -a headers
9
+
10
+ headers=(
11
+ 'previous:previous entry tasks'
12
+ 'current:current entry tasks'
13
+ 'impediments:current entry impediments'
14
+ 'notes:current entry notes'
15
+ )
16
+
17
+ _values -s , 'sub-header' $headers
18
+ }
19
+
20
+ _standup_dates() {
21
+ local directory="${STANDUP_MD_DIR:-$HOME/.cache/standup_md}"
22
+ local index
23
+ local file
24
+ local -a dates
25
+
26
+ for (( index = 1; index <= $#words; index++ )); do
27
+ case "$words[index]" in
28
+ --directory=*)
29
+ directory="${words[index]#--directory=}"
30
+ ;;
31
+ --directory|-d)
32
+ if (( index < $#words )); then
33
+ directory="$words[index + 1]"
34
+ fi
35
+ ;;
36
+ esac
37
+ done
38
+
39
+ directory=${~directory}
40
+ if [[ -d "$directory" ]]; then
41
+ for file in "$directory"/*.md(N); do
42
+ file="${file:t:r}"
43
+ if [[ "$file" == <->_<-> ]]; then
44
+ dates+=("${file/_/-}:standup file ${file}.md")
45
+ fi
46
+ done
47
+ fi
48
+
49
+ if (( ${#dates} )); then
50
+ _describe -t dates 'standup date' dates
51
+ else
52
+ _message -e dates 'date (YYYY-MM or YYYY-MM-DD)'
53
+ fi
54
+ }
55
+
56
+ _arguments -s -S \
57
+ '(- *)'{-h,--help}'[show help]' \
58
+ '(- *)--version[show version]' \
59
+ '(- *)--zsh-completion[print zsh completion setup instructions]' \
60
+ '--current[List of current entry tasks]:tasks:_standup_array_values' \
61
+ '--previous[List of previous entry tasks]:tasks:_standup_array_values' \
62
+ '--impediments[List of impediments for current entry]:impediments:_standup_array_values' \
63
+ '--notes[List of notes for current entry]:notes:_standup_array_values' \
64
+ '--sub-header-order[The order of the sub-headers when writing the file]:sub-header order:_standup_sub_header_order' \
65
+ '--indent-width[Number of spaces used for each nested task level]:indent width:' \
66
+ '(-f --file-name-format)-f[Date-formattable string to use for standup file name]:strftime format:' \
67
+ '(-f --file-name-format)--file-name-format[Date-formattable string to use for standup file name]:strftime format:' \
68
+ '(-E --editor)-E[Editor to use for opening standup files]:editor:_path_commands' \
69
+ '(-E --editor)--editor[Editor to use for opening standup files]:editor:_path_commands' \
70
+ '(-d --directory)-d[The directory where standup files are located]:directory:_directories' \
71
+ '(-d --directory)--directory[The directory where standup files are located]:directory:_directories' \
72
+ '(-w --write --no-write)'{-w,--write}'[write current entry if it does not exist]' \
73
+ '(-w --write --no-write)--no-write[do not write current entry if it does not exist]' \
74
+ '(-a --auto-fill-previous --no-auto-fill-previous)'{-a,--auto-fill-previous}'[auto-generate previous tasks for new entries]' \
75
+ '(-a --auto-fill-previous --no-auto-fill-previous)--no-auto-fill-previous[do not auto-generate previous tasks for new entries]' \
76
+ '(-e --edit --no-edit)'{-e,--edit}'[open the file in the editor]' \
77
+ '(-e --edit --no-edit)--no-edit[do not open the file in the editor]' \
78
+ '(-v --verbose --no-verbose)'{-v,--verbose}'[use verbose output]' \
79
+ '(-v --verbose --no-verbose)--no-verbose[disable verbose output]' \
80
+ '(-p --print)'{-p,--print}'[print current entry]::date:_standup_dates' \
81
+ '1:standup file date:_standup_dates'
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "standup_md/parsers/markdown"
4
+
3
5
  module StandupMD
4
6
  class Cli
5
7
  ##
@@ -20,7 +22,9 @@ module StandupMD
20
22
  next if !tasks || tasks.empty?
21
23
 
22
24
  puts sub_header(header_type)
23
- tasks.each { |task| puts "#{config.file.bullet_character} #{task}" }
25
+ entry.public_send("#{header_type}_tasks").each do |task|
26
+ puts parser.task_line(task)
27
+ end
24
28
  end
25
29
  puts
26
30
  end
@@ -69,6 +73,11 @@ module StandupMD
69
73
  "The order of the sub-headers when writing the file"
70
74
  ) { |v| config.file.sub_header_order = v }
71
75
 
76
+ opts.on(
77
+ "--indent-width INTEGER", Integer,
78
+ "Number of spaces used for each nested task level"
79
+ ) { |v| config.file.indent_width = v }
80
+
72
81
  opts.on(
73
82
  "-f", "--file-name-format STRING",
74
83
  "Date-formattable string to use for standup file name"
@@ -104,6 +113,11 @@ module StandupMD
104
113
  "Verbose output. Default is false."
105
114
  ) { |v| config.cli.verbose = v }
106
115
 
116
+ opts.on(
117
+ "--zsh-completion",
118
+ "Print zsh completion setup instructions"
119
+ ) { @zsh_completion_requested = true }
120
+
107
121
  opts.on(
108
122
  "-p", "--print [DATE]",
109
123
  "Print current entry.",
@@ -115,6 +129,12 @@ module StandupMD
115
129
  v.nil? ? Date.today : Date.strptime(v, config.file.header_date_format)
116
130
  end
117
131
  end.parse!(options)
132
+ if zsh_completion_requested?
133
+ raise OptionParser::InvalidArgument, options.join(" ") unless options.empty?
134
+
135
+ return
136
+ end
137
+
118
138
  unless options.empty?
119
139
  @file_date_argument = true
120
140
  config.cli.date = parse_file_date(options.shift)
@@ -206,7 +226,11 @@ module StandupMD
206
226
  # @return [String]
207
227
  def sub_header(header_type)
208
228
  "#" * config.file.sub_header_depth + " " +
209
- config.file.public_send("#{header_type}_header").capitalize
229
+ config.file.public_send("#{header_type}_header")
230
+ end
231
+
232
+ def parser
233
+ StandupMD::Parsers::Markdown.new(config.file)
210
234
  end
211
235
  end
212
236
  end
@@ -9,6 +9,10 @@ module StandupMD
9
9
  class Cli
10
10
  include Helpers
11
11
 
12
+ ZSH_COMPLETION_FILE = ::File.expand_path(
13
+ ::File.join(__dir__, "..", "..", "completion", "zsh", "_standup")
14
+ ).freeze
15
+
12
16
  ##
13
17
  # Access to the class's configuration.
14
18
  #
@@ -25,10 +29,45 @@ module StandupMD
25
29
  puts msg if config.verbose
26
30
  end
27
31
 
32
+ ##
33
+ # Prints zsh completion setup instructions.
34
+ #
35
+ # @return [String]
36
+ def self.zsh_completion_instructions
37
+ completion_dir = ::File.dirname(ZSH_COMPLETION_FILE)
38
+
39
+ <<~INSTRUCTIONS
40
+ Zsh completion file:
41
+ #{ZSH_COMPLETION_FILE}
42
+
43
+ To load it directly, add this before compinit runs:
44
+
45
+ fpath=("#{completion_dir}" $fpath)
46
+ autoload -Uz compinit
47
+ compinit
48
+
49
+ Or symlink it into your own completion directory:
50
+
51
+ mkdir -p ~/.zsh/completions
52
+ ln -sf "#{ZSH_COMPLETION_FILE}" ~/.zsh/completions/_standup
53
+
54
+ Then make sure that directory is in fpath before compinit runs:
55
+
56
+ fpath=(~/.zsh/completions $fpath)
57
+ autoload -Uz compinit
58
+ compinit
59
+ INSTRUCTIONS
60
+ end
61
+
28
62
  ##
29
63
  # Creates an instance of +StandupMD+ and runs what the user requested.
30
64
  def self.execute(options = [])
31
65
  new(options).tap do |exe|
66
+ if exe.zsh_completion_requested?
67
+ puts zsh_completion_instructions
68
+ next
69
+ end
70
+
32
71
  exe.write_file if exe.write?
33
72
  if config.print
34
73
  exe.print(exe.entry)
@@ -64,6 +103,14 @@ module StandupMD
64
103
  @file_date_argument
65
104
  end
66
105
 
106
+ ##
107
+ # Was zsh completion output requested?
108
+ #
109
+ # @return [Boolean]
110
+ def zsh_completion_requested?
111
+ @zsh_completion_requested
112
+ end
113
+
67
114
  ##
68
115
  # Constructor. Sets defaults.
69
116
  #
@@ -72,9 +119,14 @@ module StandupMD
72
119
  @config = self.class.config
73
120
  @preference_file_loaded = false
74
121
  @file_date_argument = false
122
+ @zsh_completion_requested = false
75
123
  @options = options
124
+ return if load_zsh_completion_request(options)
125
+
76
126
  load_preferences if load_config
77
127
  load_runtime_preferences(options)
128
+ return if zsh_completion_requested?
129
+
78
130
  @file = find_file
79
131
  @file&.load
80
132
  @entry = @file.nil? ? nil : new_entry(@file)
@@ -137,6 +189,19 @@ module StandupMD
137
189
 
138
190
  private
139
191
 
192
+ ##
193
+ # Detects zsh completion setup requests before loading user preferences.
194
+ #
195
+ # @return [Boolean]
196
+ def load_zsh_completion_request(options)
197
+ return false unless options.include?("--zsh-completion")
198
+
199
+ invalid_options = options - ["--zsh-completion"]
200
+ raise OptionParser::InvalidArgument, invalid_options.join(" ") unless invalid_options.empty?
201
+
202
+ @zsh_completion_requested = true
203
+ end
204
+
140
205
  ##
141
206
  # Is this a read-only action?
142
207
  #
@@ -20,6 +20,7 @@ module StandupMD
20
20
  sub_header_order: %w[previous current impediments notes],
21
21
  directory: ::File.join(ENV["HOME"], ".cache", "standup_md"),
22
22
  bullet_character: "-",
23
+ indent_width: 2,
23
24
  name_format: "%Y_%m.md",
24
25
  create: true
25
26
  }.freeze
@@ -56,6 +57,14 @@ module StandupMD
56
57
  # @default "-" (dash)
57
58
  attr_reader :bullet_character
58
59
 
60
+ ##
61
+ # Number of spaces used for each nested task level.
62
+ #
63
+ # @return [Integer]
64
+ #
65
+ # @default 2
66
+ attr_reader :indent_width
67
+
59
68
  ##
60
69
  # String to be used as "Current" header.
61
70
  #
@@ -186,6 +195,18 @@ module StandupMD
186
195
  @bullet_character = char
187
196
  end
188
197
 
198
+ ##
199
+ # Setter for indent_width. Must be a positive integer.
200
+ #
201
+ # @param [Integer] width
202
+ #
203
+ # @return [Integer]
204
+ def indent_width=(width)
205
+ raise "Indent width must be a positive integer" unless width.is_a?(Integer) && width.positive?
206
+
207
+ @indent_width = width
208
+ end
209
+
189
210
  ##
190
211
  # Setter for directory. Must be expanded in case the user uses `~` for
191
212
  # home. If the directory doesn't exist, it will be created. To reset
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "json"
4
+ require "standup_md/section"
4
5
 
5
6
  module StandupMD
6
7
  ##
@@ -9,6 +10,8 @@ module StandupMD
9
10
  class Entry
10
11
  include Comparable
11
12
 
13
+ SECTION_TYPES = %i[current previous impediments notes].freeze
14
+
12
15
  ##
13
16
  # Access to the class's configuration.
14
17
  #
@@ -25,30 +28,6 @@ module StandupMD
25
28
  # @return [Date]
26
29
  attr_accessor :date
27
30
 
28
- ##
29
- # The tasks for today.
30
- #
31
- # @return [Array]
32
- attr_accessor :current
33
-
34
- ##
35
- # The tasks from the previous day.
36
- #
37
- # @return [Array]
38
- attr_accessor :previous
39
-
40
- ##
41
- # Impediments for this entry.
42
- #
43
- # @return [Array]
44
- attr_accessor :impediments
45
-
46
- ##
47
- # Nnotes to add to this entry.
48
- #
49
- # @return [Array]
50
- attr_accessor :notes
51
-
52
31
  ##
53
32
  # Creates a generic entry. Default values can be set via configuration.
54
33
  # Yields the entry if a block is passed so you can change values.
@@ -81,10 +60,43 @@ module StandupMD
81
60
 
82
61
  @config = self.class.config
83
62
  @date = date
84
- @current = current
85
- @previous = previous
86
- @impediments = impediments
87
- @notes = notes
63
+ @sections = {}
64
+ self.current = current
65
+ self.previous = previous
66
+ self.impediments = impediments
67
+ self.notes = notes
68
+ end
69
+
70
+ SECTION_TYPES.each do |type|
71
+ define_method(type) do
72
+ section(type).tasks.map(&:to_s)
73
+ end
74
+
75
+ define_method("#{type}=") do |tasks|
76
+ set_section(type, tasks)
77
+ end
78
+
79
+ define_method("#{type}_tasks") do
80
+ section(type).tasks
81
+ end
82
+ end
83
+
84
+ ##
85
+ # Sections for this entry.
86
+ #
87
+ # @return [Array<StandupMD::Section>]
88
+ def sections
89
+ SECTION_TYPES.map { |type| section(type) }
90
+ end
91
+
92
+ ##
93
+ # Find a section by type.
94
+ #
95
+ # @param [Symbol, String] type
96
+ #
97
+ # @return [StandupMD::Section]
98
+ def section(type)
99
+ @sections[type.to_sym] ||= Section.new(type)
88
100
  end
89
101
 
90
102
  ##
@@ -115,5 +127,11 @@ module StandupMD
115
127
  def to_json
116
128
  to_h.to_json
117
129
  end
130
+
131
+ private
132
+
133
+ def set_section(type, tasks)
134
+ @sections[type.to_sym] = Section.new(type, tasks || [])
135
+ end
118
136
  end
119
137
  end
@@ -2,14 +2,12 @@
2
2
 
3
3
  require "date"
4
4
  require "fileutils"
5
- require "standup_md/file/helpers"
5
+ require "standup_md/parsers/markdown"
6
6
 
7
7
  module StandupMD
8
8
  ##
9
9
  # Class for handling reading and writing standup files.
10
10
  class File
11
- include StandupMD::File::Helpers
12
-
13
11
  class << self
14
12
  ##
15
13
  # Access to the class's configuration.
@@ -88,6 +86,7 @@ module StandupMD
88
86
  # @return [StandupMP::File]
89
87
  def initialize(file_name)
90
88
  @config = self.class.config
89
+ @parser = StandupMD::Parsers::Markdown.new(@config)
91
90
  if file_name.include?(::File::SEPARATOR)
92
91
  raise ArgumentError,
93
92
  "#{file_name} contains directory. Please use `StandupMD.config.file.directory=`"
@@ -137,40 +136,14 @@ module StandupMD
137
136
 
138
137
  ##
139
138
  # Loads the file's contents.
140
- # TODO clean up this method.
141
139
  #
142
140
  # @return [StandupMD::FileList]
143
141
  def load
144
142
  raise "File #{name} does not exist." unless ::File.file?(name)
145
143
 
146
- entry_list = EntryList.new
147
- record = {}
148
- section_type = ""
149
- ::File.foreach(name) do |line|
150
- line.chomp!
151
- next if line.strip.empty?
152
-
153
- if header?(line)
154
- unless record.empty?
155
- entry_list << new_entry(record)
156
- record = {}
157
- end
158
- record["header"] = line.sub(/^\#{#{@config.header_depth}}\s*/, "")
159
- section_type = @config.notes_header
160
- record[section_type] = []
161
- elsif sub_header?(line)
162
- section_type = determine_section_type(line)
163
- record[section_type] = []
164
- else
165
- record[section_type] << line.sub(bullet_character_regex, "")
166
- end
167
- end
168
- entry_list << new_entry(record) unless record.empty?
169
144
  @loaded = true
170
- @entries = entry_list.sort
145
+ @entries = @parser.read(name)
171
146
  self
172
- rescue => e
173
- raise "File malformation: #{e}"
174
147
  end
175
148
 
176
149
  ##
@@ -185,20 +158,7 @@ module StandupMD
185
158
  sorted_entries = entries.sort
186
159
  start_date = dates.fetch(:start_date, sorted_entries.first.date)
187
160
  end_date = dates.fetch(:end_date, sorted_entries.last.date)
188
- ::File.open(name, "w") do |f|
189
- sorted_entries.filter(start_date, end_date).sort_reverse.each do |entry|
190
- f.puts header(entry.date)
191
- @config.sub_header_order.each do |attr|
192
- tasks = entry.public_send(attr)
193
- next if !tasks || tasks.empty?
194
-
195
- f.puts sub_header(@config.public_send("#{attr}_header").capitalize)
196
- tasks.each { |task| f.puts "#{@config.bullet_character} #{task}" }
197
- end
198
- f.puts
199
- end
200
- end
201
- true
161
+ @parser.write(name, sorted_entries, start_date: start_date, end_date: end_date)
202
162
  end
203
163
  end
204
164
  end
@@ -0,0 +1,168 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
4
+ require "standup_md/entry"
5
+ require "standup_md/entry_list"
6
+ require "standup_md/section"
7
+ require "standup_md/task"
8
+ require "standup_md/title"
9
+
10
+ module StandupMD
11
+ module Parsers
12
+ ##
13
+ # Parser and renderer for the markdown standup format.
14
+ class Markdown
15
+ ##
16
+ # Access to file configuration.
17
+ #
18
+ # @return [StandupMD::Config::File]
19
+ attr_reader :config
20
+
21
+ ##
22
+ # Constructs an instance of +StandupMD::Parsers::Markdown+.
23
+ #
24
+ # @param [StandupMD::Config::File] config
25
+ def initialize(config = StandupMD.config.file)
26
+ @config = config
27
+ end
28
+
29
+ ##
30
+ # Reads entries from a markdown standup file.
31
+ #
32
+ # @param [String] file_name
33
+ #
34
+ # @return [StandupMD::EntryList]
35
+ def read(file_name)
36
+ entry_list = EntryList.new
37
+ record = nil
38
+ section = nil
39
+
40
+ ::File.foreach(file_name) do |line|
41
+ line.chomp!
42
+ next if line.strip.empty?
43
+
44
+ if header?(line)
45
+ entry_list << entry(record) if record
46
+ record = {title: title(line), sections: {}}
47
+ section = section(:notes)
48
+ record[:sections][:notes] = section
49
+ elsif sub_header?(line)
50
+ section = section(section_type(line))
51
+ record[:sections][section.type] = section
52
+ else
53
+ section << task(line)
54
+ end
55
+ end
56
+
57
+ entry_list << entry(record) if record
58
+ entry_list.sort
59
+ rescue => e
60
+ raise "File malformation: #{e}"
61
+ end
62
+
63
+ ##
64
+ # Writes entries to a markdown standup file.
65
+ #
66
+ # @param [String] file_name
67
+ # @param [StandupMD::EntryList] entries
68
+ # @param [Date] start_date
69
+ # @param [Date] end_date
70
+ #
71
+ # @return [Boolean]
72
+ def write(file_name, entries, start_date:, end_date:)
73
+ ::File.open(file_name, "w") do |f|
74
+ entries.filter(start_date, end_date).sort_reverse.each do |entry|
75
+ f.puts Title.new(entry.date).to_markdown
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
86
+ end
87
+
88
+ ##
89
+ # Renders a task as a markdown list item.
90
+ #
91
+ # @param [String, StandupMD::Task] task
92
+ #
93
+ # @return [String]
94
+ def task_line(task)
95
+ build_task(task).to_markdown
96
+ end
97
+
98
+ private
99
+
100
+ def header?(line)
101
+ line.match?(header_regex)
102
+ end
103
+
104
+ def sub_header?(line)
105
+ line.match?(sub_header_regex)
106
+ end
107
+
108
+ def header_regex
109
+ /^#{"#" * config.header_depth}\s+/
110
+ end
111
+
112
+ def sub_header_regex
113
+ /^#{"#" * config.sub_header_depth}\s+/
114
+ end
115
+
116
+ def title(line)
117
+ line.sub(/^\#{#{config.header_depth}}\s*/, "")
118
+ end
119
+
120
+ def section(type)
121
+ Section.new(type)
122
+ end
123
+
124
+ def section_type(line)
125
+ sub_header = line.sub(/^\#{#{config.sub_header_depth}}\s*/, "")
126
+ Entry::SECTION_TYPES.each do |type|
127
+ header = config.public_send("#{type}_header")
128
+ return type if sub_header.include?(header)
129
+ end
130
+ raise "Unrecognized header [#{sub_header}]"
131
+ end
132
+
133
+ def task(line)
134
+ match = line.match(task_regex)
135
+ return Task.new(line) unless match
136
+
137
+ Task.new(
138
+ match[:text],
139
+ indent_level: match[:indent].size / config.indent_width
140
+ )
141
+ end
142
+
143
+ def task_regex
144
+ /\A(?<indent>\s*)#{Regexp.escape(config.bullet_character)}\s*(?<text>.*)\z/
145
+ end
146
+
147
+ def entry(record)
148
+ Entry.new(
149
+ Date.strptime(record[:title], config.header_date_format),
150
+ tasks(record, :current),
151
+ tasks(record, :previous),
152
+ tasks(record, :impediments),
153
+ tasks(record, :notes)
154
+ )
155
+ end
156
+
157
+ def tasks(record, type)
158
+ record[:sections].fetch(type, Section.new(type)).tasks
159
+ end
160
+
161
+ def build_task(task)
162
+ return task if task.is_a?(Task)
163
+
164
+ Task.new(task)
165
+ end
166
+ end
167
+ end
168
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "standup_md/task"
4
+
5
+ module StandupMD
6
+ ##
7
+ # A named section of a standup entry, such as current, previous,
8
+ # impediments, or notes.
9
+ class Section
10
+ ##
11
+ # The semantic section type.
12
+ #
13
+ # @return [Symbol]
14
+ attr_reader :type
15
+
16
+ ##
17
+ # Tasks for the section.
18
+ #
19
+ # @return [Array<StandupMD::Task>]
20
+ attr_reader :tasks
21
+
22
+ ##
23
+ # Constructs an instance of +StandupMD::Section+.
24
+ #
25
+ # @param [Symbol, String] type
26
+ # @param [Array<String, StandupMD::Task>] tasks
27
+ def initialize(type, tasks = [])
28
+ @type = type.to_sym
29
+ @tasks = tasks.map { |task| build_task(task) }
30
+ end
31
+
32
+ ##
33
+ # Adds a task to the section.
34
+ #
35
+ # @param [String, StandupMD::Task] task
36
+ #
37
+ # @return [Array<StandupMD::Task>]
38
+ def <<(task)
39
+ tasks << build_task(task)
40
+ end
41
+
42
+ ##
43
+ # Is the section empty?
44
+ #
45
+ # @return [Boolean]
46
+ def empty?
47
+ tasks.empty?
48
+ end
49
+
50
+ ##
51
+ # The configured section heading.
52
+ #
53
+ # @return [String]
54
+ def to_s
55
+ StandupMD.config.file.public_send("#{type}_header")
56
+ end
57
+
58
+ ##
59
+ # The section rendered as markdown lines.
60
+ #
61
+ # @return [Array<String>]
62
+ def to_markdown
63
+ [
64
+ "#" * StandupMD.config.file.sub_header_depth + " " + to_s,
65
+ *tasks.map(&:to_markdown)
66
+ ]
67
+ end
68
+
69
+ private
70
+
71
+ def build_task(task)
72
+ return task if task.is_a?(Task)
73
+
74
+ Task.new(task)
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StandupMD
4
+ ##
5
+ # A single standup task. The text stays format-neutral, while indentation
6
+ # level lets parsers render nested tasks for their own formats.
7
+ class Task
8
+ ##
9
+ # The task text.
10
+ #
11
+ # @return [String]
12
+ attr_reader :text
13
+
14
+ ##
15
+ # The nesting level of the task.
16
+ #
17
+ # @return [Integer]
18
+ attr_reader :indent_level
19
+
20
+ ##
21
+ # Constructs an instance of +StandupMD::Task+.
22
+ #
23
+ # @param [String] text
24
+ # @param [Integer] indent_level
25
+ def initialize(text, indent_level: 0)
26
+ unless indent_level.is_a?(Integer) && !indent_level.negative?
27
+ raise ArgumentError, "Indent level must be a non-negative integer"
28
+ end
29
+
30
+ @text = text.to_s
31
+ @indent_level = indent_level
32
+ end
33
+
34
+ ##
35
+ # The format-neutral task text.
36
+ #
37
+ # @return [String]
38
+ def to_s
39
+ text
40
+ end
41
+
42
+ ##
43
+ # The task rendered as a markdown list item.
44
+ #
45
+ # @return [String]
46
+ def to_markdown
47
+ indent = " " * StandupMD.config.file.indent_width * indent_level
48
+ "#{indent}#{StandupMD.config.file.bullet_character} #{text}"
49
+ end
50
+
51
+ ##
52
+ # Compares task contents.
53
+ def ==(other)
54
+ return text == other if other.is_a?(String)
55
+ return false unless other.is_a?(Task)
56
+
57
+ text == other.text && indent_level == other.indent_level
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
4
+
5
+ module StandupMD
6
+ ##
7
+ # The title for a standup entry.
8
+ class Title
9
+ ##
10
+ # The entry date.
11
+ #
12
+ # @return [Date]
13
+ attr_reader :date
14
+
15
+ ##
16
+ # Constructs an instance of +StandupMD::Title+.
17
+ #
18
+ # @param [Date] date
19
+ def initialize(date)
20
+ raise ArgumentError, "Must be a Date object" unless date.is_a?(Date)
21
+
22
+ @date = date
23
+ end
24
+
25
+ ##
26
+ # The configured title text.
27
+ #
28
+ # @return [String]
29
+ def to_s
30
+ date.strftime(StandupMD.config.file.header_date_format)
31
+ end
32
+
33
+ ##
34
+ # The title rendered as markdown.
35
+ #
36
+ # @return [String]
37
+ def to_markdown
38
+ "#" * StandupMD.config.file.header_depth + " " + to_s
39
+ end
40
+ end
41
+ end
@@ -9,19 +9,19 @@ module StandupMD
9
9
  # Major version.
10
10
  #
11
11
  # @return [Integer]
12
- MAJOR = 0
12
+ MAJOR = 1
13
13
 
14
14
  ##
15
15
  # Minor version.
16
16
  #
17
17
  # @return [Integer]
18
- MINOR = 3
18
+ MINOR = 0
19
19
 
20
20
  ##
21
21
  # Patch version.
22
22
  #
23
23
  # @return [Integer]
24
- PATCH = 16
24
+ PATCH = 0
25
25
 
26
26
  ##
27
27
  # Version as +[MAJOR, MINOR, PATCH]+
data/lib/standup_md.rb CHANGED
@@ -1,11 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "standup_md/version"
4
- require "standup_md/file"
4
+ require "standup_md/config"
5
+ require "standup_md/task"
6
+ require "standup_md/title"
7
+ require "standup_md/section"
5
8
  require "standup_md/entry"
6
9
  require "standup_md/entry_list"
10
+ require "standup_md/parsers/markdown"
11
+ require "standup_md/file"
7
12
  require "standup_md/cli"
8
- require "standup_md/config"
9
13
 
10
14
  ##
11
15
  # The main module for the gem. Provides access to configuration classes.
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: standup_md
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.16
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Evan Gray
@@ -107,6 +107,7 @@ files:
107
107
  - Rakefile
108
108
  - _config.yml
109
109
  - bin/standup
110
+ - completion/zsh/_standup
110
111
  - lib/standup_md.rb
111
112
  - lib/standup_md/cli.rb
112
113
  - lib/standup_md/cli/helpers.rb
@@ -118,7 +119,10 @@ files:
118
119
  - lib/standup_md/entry.rb
119
120
  - lib/standup_md/entry_list.rb
120
121
  - lib/standup_md/file.rb
121
- - lib/standup_md/file/helpers.rb
122
+ - lib/standup_md/parsers/markdown.rb
123
+ - lib/standup_md/section.rb
124
+ - lib/standup_md/task.rb
125
+ - lib/standup_md/title.rb
122
126
  - lib/standup_md/version.rb
123
127
  - standup_md.gemspec
124
128
  homepage: https://evanthegrayt.github.io/standup_md/
@@ -1,65 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module StandupMD
4
- class File
5
- ##
6
- # Module responsible for reading and writing standup files.
7
- module Helpers # :nodoc:
8
- private
9
-
10
- def header?(line) # :nodoc:
11
- line.match(header_regex)
12
- end
13
-
14
- def sub_header?(line) # :nodoc:
15
- line.match(sub_header_regex)
16
- end
17
-
18
- def header_regex # :nodoc:
19
- /^#{"#" * StandupMD.config.file.header_depth}\s+/
20
- end
21
-
22
- def sub_header_regex # :nodoc:
23
- /^#{"#" * StandupMD.config.file.sub_header_depth}\s+/
24
- end
25
-
26
- def bullet_character_regex # :nodoc:
27
- /\s*#{StandupMD.config.file.bullet_character}\s*/
28
- end
29
-
30
- def determine_section_type(line) # :nodoc:
31
- line = line.sub(/^\#{#{StandupMD.config.file.sub_header_depth}}\s*/, "")
32
- [
33
- StandupMD.config.file.current_header,
34
- StandupMD.config.file.previous_header,
35
- StandupMD.config.file.impediments_header,
36
- StandupMD.config.file.notes_header
37
- ].each { |header| return header if line.include?(header) }
38
- raise "Unrecognized header [#{line}]"
39
- end
40
-
41
- def new_entry(record) # :nodoc:
42
- Entry.new(
43
- Date.strptime(
44
- record["header"],
45
- StandupMD.config.file.header_date_format
46
- ),
47
- record[StandupMD.config.file.current_header],
48
- record[StandupMD.config.file.previous_header],
49
- record[StandupMD.config.file.impediments_header],
50
- record[StandupMD.config.file.notes_header]
51
- )
52
- end
53
-
54
- def header(date)
55
- "#" * StandupMD.config.file.header_depth +
56
- " " +
57
- date.strftime(StandupMD.config.file.header_date_format)
58
- end
59
-
60
- def sub_header(subhead)
61
- "#" * StandupMD.config.file.sub_header_depth + " " + subhead
62
- end
63
- end
64
- end
65
- end