doing 2.1.40 → 2.1.41

Sign up to get free protection for your applications and to get access to all the features.
Files changed (192) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +1 -1
  3. data/CHANGELOG.md +22 -0
  4. data/Gemfile.lock +1 -1
  5. data/Rakefile +4 -4
  6. data/bin/commands/changes.rb +1 -1
  7. data/bin/commands/tag_dir.rb +49 -15
  8. data/{Dockerfile → docker/Dockerfile} +3 -1
  9. data/{Dockerfile-2.6 → docker/Dockerfile-2.6} +2 -2
  10. data/{Dockerfile-2.7 → docker/Dockerfile-2.7} +2 -2
  11. data/{Dockerfile-3.0 → docker/Dockerfile-3.0} +2 -2
  12. data/{bash_profile → docker/bash_profile} +0 -0
  13. data/{inputrc → docker/inputrc} +0 -0
  14. data/docs/doc/Array.html +84 -2
  15. data/docs/doc/BooleanTermParser/Clause.html +1 -1
  16. data/docs/doc/BooleanTermParser/Operator.html +1 -1
  17. data/docs/doc/BooleanTermParser/Query.html +1 -1
  18. data/docs/doc/BooleanTermParser/QueryParser.html +1 -1
  19. data/docs/doc/BooleanTermParser/QueryTransformer.html +1 -1
  20. data/docs/doc/BooleanTermParser.html +1 -1
  21. data/docs/doc/Doing/ArrayNestedHash.html +198 -0
  22. data/docs/doc/Doing/ArrayTags.html +424 -0
  23. data/docs/doc/Doing/CSVExport.html +266 -0
  24. data/docs/doc/Doing/CalendarImport.html +232 -0
  25. data/docs/doc/Doing/Change.html +617 -0
  26. data/docs/doc/Doing/Changes.html +468 -0
  27. data/docs/doc/Doing/ChronifyArray.html +347 -0
  28. data/docs/doc/Doing/ChronifyNumeric.html +271 -0
  29. data/docs/doc/Doing/ChronifyString.html +682 -0
  30. data/docs/doc/Doing/Color.html +2 -2
  31. data/docs/doc/Doing/Completion/BashCompletions.html +445 -0
  32. data/docs/doc/Doing/Completion/FishCompletions.html +445 -0
  33. data/docs/doc/Doing/Completion/StringUtils.html +229 -0
  34. data/docs/doc/Doing/Completion/ZshCompletions.html +445 -0
  35. data/docs/doc/Doing/Completion.html +17 -3
  36. data/docs/doc/Doing/Configuration.html +1 -1
  37. data/docs/doc/Doing/DayOneRenderer.html +383 -0
  38. data/docs/doc/Doing/DayoneExport.html +290 -0
  39. data/docs/doc/Doing/DoingImport.html +391 -0
  40. data/docs/doc/Doing/Entry.html +381 -0
  41. data/docs/doc/Doing/Errors/DoingNoTraceError.html +1 -1
  42. data/docs/doc/Doing/Errors/DoingRuntimeError.html +1 -1
  43. data/docs/doc/Doing/Errors/DoingStandardError.html +1 -1
  44. data/docs/doc/Doing/Errors/EmptyInput.html +1 -1
  45. data/docs/doc/Doing/Errors/HistoryLimitError.html +1 -1
  46. data/docs/doc/Doing/Errors/InvalidPlugin.html +1 -1
  47. data/docs/doc/Doing/Errors/MissingBackupFile.html +1 -1
  48. data/docs/doc/Doing/Errors/NoResults.html +1 -1
  49. data/docs/doc/Doing/Errors/PluginException.html +1 -1
  50. data/docs/doc/Doing/Errors/UserCancelled.html +1 -1
  51. data/docs/doc/Doing/Errors/WrongCommand.html +1 -1
  52. data/docs/doc/Doing/Errors.html +1 -1
  53. data/docs/doc/Doing/HTMLExport.html +256 -0
  54. data/docs/doc/Doing/Hooks.html +1 -1
  55. data/docs/doc/Doing/Item.html +47 -3
  56. data/docs/doc/Doing/ItemDates.html +564 -0
  57. data/docs/doc/Doing/ItemQuery.html +614 -0
  58. data/docs/doc/Doing/ItemState.html +387 -0
  59. data/docs/doc/Doing/ItemTags.html +498 -0
  60. data/docs/doc/Doing/Items.html +460 -11
  61. data/docs/doc/Doing/JSONExport.html +222 -0
  62. data/docs/doc/Doing/Logger.html +1 -1
  63. data/docs/doc/Doing/MarkdownExport.html +266 -0
  64. data/docs/doc/Doing/MarkdownRenderer.html +383 -0
  65. data/docs/doc/Doing/Note.html +16 -3
  66. data/docs/doc/Doing/Pager.html +1 -1
  67. data/docs/doc/Doing/Plugins.html +1 -1
  68. data/docs/doc/Doing/Prompt.html +31 -682
  69. data/docs/doc/Doing/PromptChoose.html +484 -0
  70. data/docs/doc/Doing/PromptFZF.html +391 -0
  71. data/docs/doc/Doing/PromptInput.html +572 -0
  72. data/docs/doc/Doing/PromptSTD.html +293 -0
  73. data/docs/doc/Doing/PromptYN.html +237 -0
  74. data/docs/doc/Doing/Section.html +58 -2
  75. data/docs/doc/Doing/StringHighlight.html +533 -0
  76. data/docs/doc/Doing/StringNormalize.html +929 -0
  77. data/docs/doc/Doing/StringQuery.html +725 -0
  78. data/docs/doc/Doing/StringTags.html +884 -0
  79. data/docs/doc/Doing/StringTransform.html +565 -0
  80. data/docs/doc/Doing/StringTruncate.html +448 -0
  81. data/docs/doc/Doing/StringURL.html +409 -0
  82. data/docs/doc/Doing/SymbolNormalize.html +341 -0
  83. data/docs/doc/Doing/TaskPaperExport.html +222 -0
  84. data/docs/doc/Doing/TemplateExport.html +249 -0
  85. data/docs/doc/Doing/TemplateString.html +101 -2
  86. data/docs/doc/Doing/TimingImport.html +285 -0
  87. data/docs/doc/Doing/Types.html +1 -1
  88. data/docs/doc/Doing/Util/Backup.html +9 -7
  89. data/docs/doc/Doing/Util.html +2 -2
  90. data/docs/doc/Doing/Version.html +523 -0
  91. data/docs/doc/Doing/WWID/WWIDUtil.html +510 -0
  92. data/docs/doc/Doing/WWID.html +4377 -217
  93. data/docs/doc/Doing/WWIDDisplay.html +865 -0
  94. data/docs/doc/Doing/WWIDEditor.html +466 -0
  95. data/docs/doc/Doing/WWIDFileTools.html +359 -0
  96. data/docs/doc/Doing/WWIDFilter.html +466 -0
  97. data/docs/doc/Doing/WWIDGuess.html +299 -0
  98. data/docs/doc/Doing/WWIDInteractive.html +752 -0
  99. data/docs/doc/Doing/WWIDModify.html +1078 -0
  100. data/docs/doc/Doing/WWIDTags.html +302 -0
  101. data/docs/doc/Doing/WWIDTimers.html +359 -0
  102. data/docs/doc/Doing/WWIDUtil.html +510 -0
  103. data/docs/doc/Doing.html +9 -6
  104. data/docs/doc/FalseClass.html +1 -1
  105. data/docs/doc/GLI/Commands/Help.html +1 -1
  106. data/docs/doc/GLI/Commands/MarkdownDocumentListener.html +1 -1
  107. data/docs/doc/GLI/Commands.html +1 -1
  108. data/docs/doc/GLI.html +1 -1
  109. data/docs/doc/Hash.html +1 -1
  110. data/docs/doc/Numeric.html +23 -78
  111. data/docs/doc/Object.html +1 -1
  112. data/docs/doc/PhraseParser/Operator.html +1 -1
  113. data/docs/doc/PhraseParser/PhraseClause.html +1 -1
  114. data/docs/doc/PhraseParser/Query.html +1 -1
  115. data/docs/doc/PhraseParser/QueryParser.html +1 -1
  116. data/docs/doc/PhraseParser/QueryTransformer.html +1 -1
  117. data/docs/doc/PhraseParser/TermClause.html +1 -1
  118. data/docs/doc/PhraseParser.html +1 -1
  119. data/docs/doc/Status.html +1 -1
  120. data/docs/doc/String.html +58 -633
  121. data/docs/doc/Symbol.html +9 -224
  122. data/docs/doc/Time.html +119 -13
  123. data/docs/doc/TrueClass.html +1 -1
  124. data/docs/doc/_index.html +324 -8
  125. data/docs/doc/class_list.html +1 -1
  126. data/docs/doc/file.README.html +1 -1
  127. data/docs/doc/index.html +1 -1
  128. data/docs/doc/method_list.html +2326 -542
  129. data/docs/doc/top-level-namespace.html +2 -2
  130. data/doing.rdoc +13 -3
  131. data/lib/completion/_doing.zsh +1 -1
  132. data/lib/completion/doing.bash +2 -2
  133. data/lib/completion/doing.fish +3 -1
  134. data/lib/doing/array/array.rb +16 -12
  135. data/lib/doing/array/nested_hash.rb +1 -1
  136. data/lib/doing/array/tags.rb +6 -5
  137. data/lib/doing/changelog/changelog.rb +6 -0
  138. data/lib/doing/chronify/array.rb +1 -3
  139. data/lib/doing/chronify/chronify.rb +12 -0
  140. data/lib/doing/chronify/numeric.rb +3 -2
  141. data/lib/doing/chronify/string.rb +1 -1
  142. data/lib/doing/completion/completion_string.rb +25 -0
  143. data/lib/doing/completion.rb +1 -1
  144. data/lib/doing/good.rb +8 -0
  145. data/lib/doing/item/dates.rb +1 -1
  146. data/lib/doing/{item.rb → item/item.rb} +10 -5
  147. data/lib/doing/item/query.rb +1 -1
  148. data/lib/doing/item/state.rb +1 -1
  149. data/lib/doing/item/tags.rb +1 -1
  150. data/lib/doing/items/filter.rb +67 -0
  151. data/lib/doing/items/items.rb +57 -0
  152. data/lib/doing/items/modify.rb +36 -0
  153. data/lib/doing/items/sections.rb +83 -0
  154. data/lib/doing/items/util.rb +74 -0
  155. data/lib/doing/normalize.rb +10 -2
  156. data/lib/doing/plugins/export/markdown_export.rb +4 -2
  157. data/lib/doing/plugins/import/doing_import.rb +1 -1
  158. data/lib/doing/prompt/choose.rb +118 -0
  159. data/lib/doing/prompt/fzf.rb +84 -0
  160. data/lib/doing/prompt/input.rb +129 -0
  161. data/lib/doing/prompt/prompt.rb +41 -0
  162. data/lib/doing/prompt/std.rb +32 -0
  163. data/lib/doing/prompt/yn.rb +64 -0
  164. data/lib/doing/section.rb +4 -0
  165. data/lib/doing/string/highlight.rb +1 -1
  166. data/lib/doing/string/query.rb +1 -1
  167. data/lib/doing/string/string.rb +18 -7
  168. data/lib/doing/string/tags.rb +14 -3
  169. data/lib/doing/string/transform.rb +1 -1
  170. data/lib/doing/string/truncate.rb +1 -1
  171. data/lib/doing/string/url.rb +1 -1
  172. data/lib/doing/time.rb +19 -1
  173. data/lib/doing/util_backup.rb +2 -2
  174. data/lib/doing/version.rb +1 -1
  175. data/lib/doing/wwid/display.rb +357 -360
  176. data/lib/doing/wwid/editor.rb +173 -176
  177. data/lib/doing/wwid/filetools.rb +156 -159
  178. data/lib/doing/wwid/filter.rb +191 -183
  179. data/lib/doing/wwid/guess.rb +58 -60
  180. data/lib/doing/wwid/interactive.rb +332 -330
  181. data/lib/doing/wwid/modify.rb +509 -512
  182. data/lib/doing/wwid/tags.rb +38 -41
  183. data/lib/doing/wwid/timers.rb +293 -296
  184. data/lib/doing/{wwid.rb → wwid/wwid.rb} +32 -23
  185. data/lib/doing/wwid/wwidutil.rb +79 -82
  186. data/lib/doing.rb +5 -5
  187. data/lib/helpers/threaded_tests.rb +1 -0
  188. metadata +76 -14
  189. data/lib/doing/changelog.rb +0 -6
  190. data/lib/doing/completion/string.rb +0 -17
  191. data/lib/doing/items.rb +0 -221
  192. data/lib/doing/prompt.rb +0 -330
@@ -2,616 +2,613 @@
2
2
 
3
3
  module Doing
4
4
  class WWID
5
- # Item modification methods for WWID class
6
- module Modify
7
- ##
8
- ## Adds an entry
9
- ##
10
- ## @param title [String] The entry title
11
- ## @param section [String] The section to add to
12
- ## @param opt [Hash] Additional Options
13
- ##
14
- ## @option opt :date [Date] item start date
15
- ## @option opt :note [Array] item note (will be converted if value is String)
16
- ## @option opt :back [Date] backdate
17
- ## @option opt :timed [Boolean] new item is timed entry, marks previous entry as @done
18
- ## @option opt :done [Date] If set, adds a @done tag to new entry
19
- ##
20
- def add_item(title, section = nil, opt)
21
- opt ||= {}
22
- section ||= Doing.setting('current_section')
23
- @content.add_section(section, log: false)
24
- opt[:back] ||= opt[:date] ? opt[:date] : Time.now
25
- opt[:date] ||= Time.now
26
- note = Note.new
27
- opt[:timed] ||= false
28
-
29
- note.add(opt[:note]) if opt[:note]
30
-
31
- title = [title.strip.cap_first]
32
- title = title.join(' ')
33
-
34
- if Doing.auto_tag
35
- title = autotag(title)
36
- title.add_tags!(Doing.setting('default_tags')) if Doing.setting('default_tags').good?
37
- end
5
+ ##
6
+ ## Adds an entry
7
+ ##
8
+ ## @param title [String] The entry title
9
+ ## @param section [String] The section to add to
10
+ ## @param opt [Hash] Additional Options
11
+ ##
12
+ ## @option opt :date [Date] item start date
13
+ ## @option opt :note [Note] item note (will be converted if value is String)
14
+ ## @option opt :back [Date] backdate
15
+ ## @option opt :timed [Boolean] new item is timed entry, marks previous entry as @done
16
+ ## @option opt :done [Date] If set, adds a @done tag to new entry
17
+ ##
18
+ def add_item(title, section = nil, opt)
19
+ opt ||= {}
20
+ section ||= Doing.setting('current_section')
21
+ @content.add_section(section, log: false)
22
+ opt[:back] ||= opt[:date] ? opt[:date] : Time.now
23
+ opt[:date] ||= Time.now
24
+ note = Note.new
25
+ opt[:timed] ||= false
26
+
27
+ note.add(opt[:note]) if opt[:note]
28
+
29
+ title = [title.strip.cap_first]
30
+ title = title.join(' ')
31
+
32
+ if Doing.auto_tag
33
+ title = autotag(title)
34
+ title.add_tags!(Doing.setting('default_tags')) if Doing.setting('default_tags').good?
35
+ end
38
36
 
39
- title.compress!
40
- entry = Item.new(opt[:back], title.strip, section)
37
+ title.compress!
38
+ entry = Item.new(opt[:back], title.strip, section)
41
39
 
42
- if opt[:done] && entry.should_finish?
43
- if entry.should_time?
44
- entry.tag('done', value: opt[:done])
45
- else
46
- entry.tag('done')
47
- end
40
+ if opt[:done] && entry.should_finish?
41
+ if entry.should_time?
42
+ entry.tag('done', value: opt[:done])
43
+ else
44
+ entry.tag('done')
48
45
  end
46
+ end
49
47
 
50
- entry.note = note
48
+ entry.note = note
51
49
 
52
- items = @content.clone
53
- if opt[:timed]
54
- items.reverse!
55
- items.each_with_index do |i, x|
56
- next if i.title =~ / @done/
50
+ items = @content.clone
51
+ if opt[:timed]
52
+ items.reverse!
53
+ items.each_with_index do |i, x|
54
+ next if i.title =~ / @done/
57
55
 
58
- finish_date = verify_duration(i.date, opt[:back], title: i.title)
59
- items[x].tag('done', value: finish_date.strftime('%F %R'))
60
- break
61
- end
56
+ finish_date = verify_duration(i.date, opt[:back], title: i.title)
57
+ items[x].tag('done', value: finish_date.strftime('%F %R'))
58
+ break
62
59
  end
60
+ end
61
+
62
+ Hooks.trigger :pre_entry_add, self, entry
63
63
 
64
- Hooks.trigger :pre_entry_add, self, entry
64
+ @content.push(entry)
65
+ # logger.count(:added, level: :debug)
66
+ logger.info('New entry:', %(added "#{entry.date.relative_date}: #{entry.title}" to #{section}))
65
67
 
66
- @content.push(entry)
67
- # logger.count(:added, level: :debug)
68
- logger.info('New entry:', %(added "#{entry.date.relative_date}: #{entry.title}" to #{section}))
68
+ Hooks.trigger :post_entry_added, self, entry
69
+ entry
70
+ end
69
71
 
70
- Hooks.trigger :post_entry_added, self, entry
71
- entry
72
+ # Reset start date to current time, optionally remove
73
+ # done tag (resume)
74
+ #
75
+ # @param item [Item] the item to reset/resume
76
+ # @param resume [Boolean] removing @done tag if true
77
+ #
78
+ def reset_item(item, date: nil, finish_date: nil, resume: false)
79
+ date ||= Time.now
80
+ item.date = date
81
+ if finish_date
82
+ item.tag('done', remove: true)
83
+ item.tag('done', value: finish_date.strftime('%F %R'))
84
+ else
85
+ item.tag('done', remove: true) if resume
72
86
  end
87
+ logger.info('Reset:', %(Reset #{resume ? 'and resumed ' : ''} "#{item.title}" in #{item.section}))
88
+ item
89
+ end
73
90
 
74
- # Reset start date to current time, optionally remove
75
- # done tag (resume)
76
- #
77
- # @param item [Item] the item to reset/resume
78
- # @param resume [Boolean] removing @done tag if true
79
- #
80
- def reset_item(item, date: nil, finish_date: nil, resume: false)
81
- date ||= Time.now
82
- item.date = date
83
- if finish_date
84
- item.tag('done', remove: true)
85
- item.tag('done', value: finish_date.strftime('%F %R'))
91
+ # Duplicate an item and add it as a new item
92
+ #
93
+ # @param item [Item] the item to duplicate
94
+ # @param opt [Hash] additional options
95
+ #
96
+ # @option opt :editor [Boolean] open new item in editor
97
+ # @option opt :date [String] set start date
98
+ # @option opt :in [String] add new item to section :in
99
+ # @option opt :note [Note] add note to new item
100
+ #
101
+ # @return nothing
102
+ #
103
+ def repeat_item(item, opt)
104
+ opt ||= {}
105
+ old_item = item.clone
106
+ if item.should_finish?
107
+ if item.should_time?
108
+ finish_date = verify_duration(item.date, Time.now, title: item.title)
109
+ item.title.tag!('done', value: finish_date.strftime('%F %R'))
86
110
  else
87
- item.tag('done', remove: true) if resume
111
+ item.title.tag!('done')
88
112
  end
89
- logger.info('Reset:', %(Reset #{resume ? 'and resumed ' : ''} "#{item.title}" in #{item.section}))
90
- item
113
+ Hooks.trigger :post_entry_updated, self, item, old_item
91
114
  end
92
115
 
93
- # Duplicate an item and add it as a new item
94
- #
95
- # @param item [Item] the item to duplicate
96
- # @param opt [Hash] additional options
97
- #
98
- # @option opt :editor [Boolean] open new item in editor
99
- # @option opt :date [String] set start date
100
- # @option opt :in [String] add new item to section :in
101
- # @option opt :note [Note] add note to new item
102
- #
103
- # @return nothing
104
- #
105
- def repeat_item(item, opt)
106
- opt ||= {}
107
- old_item = item.clone
108
- if item.should_finish?
109
- if item.should_time?
110
- finish_date = verify_duration(item.date, Time.now, title: item.title)
111
- item.title.tag!('done', value: finish_date.strftime('%F %R'))
112
- else
113
- item.title.tag!('done')
114
- end
115
- Hooks.trigger :post_entry_updated, self, item, old_item
116
- end
117
-
118
- # Remove @done tag
119
- title = item.title.sub(/\s*@done(\(.*?\))?/, '').chomp
120
- section = opt[:in].nil? ? item.section : guess_section(opt[:in])
121
- Doing.auto_tag = false
116
+ # Remove @done tag
117
+ title = item.title.sub(/\s*@done(\(.*?\))?/, '').chomp
118
+ section = opt[:in].nil? ? item.section : guess_section(opt[:in])
119
+ Doing.auto_tag = false
122
120
 
123
- note = opt[:note] || Note.new
121
+ note = opt[:note] || Note.new
124
122
 
125
- if opt[:editor]
126
- start = opt[:date] ? opt[:date] : Time.now
127
- to_edit = "#{start.strftime('%F %R')} | #{title}"
128
- to_edit += "\n#{note.strip_lines.join("\n")}" unless note.empty?
129
- new_item = fork_editor(to_edit)
130
- date, title, note = format_input(new_item)
123
+ if opt[:editor]
124
+ start = opt[:date] ? opt[:date] : Time.now
125
+ to_edit = "#{start.strftime('%F %R')} | #{title}"
126
+ to_edit += "\n#{note.strip_lines.join("\n")}" unless note.empty?
127
+ new_item = fork_editor(to_edit)
128
+ date, title, note = format_input(new_item)
131
129
 
132
- opt[:date] = date unless date.nil?
130
+ opt[:date] = date unless date.nil?
133
131
 
134
- if title.nil? || title.empty?
135
- logger.warn('Skipped:', 'No content provided')
136
- return
137
- end
132
+ if title.nil? || title.empty?
133
+ logger.warn('Skipped:', 'No content provided')
134
+ return
138
135
  end
139
-
140
- # @content.update_item(original, item)
141
- add_item(title, section, { note: note, back: opt[:date], timed: false })
142
136
  end
143
137
 
144
- ##
145
- ## Restart the last entry
146
- ##
147
- ## @param opt [Hash] Additional Options
148
- ##
149
- def repeat_last(opt)
150
- opt ||= {}
151
- opt[:section] ||= 'all'
152
- opt[:section] = guess_section(opt[:section])
153
- opt[:note] ||= []
154
- opt[:tag] ||= []
155
- opt[:tag_bool] ||= :and
156
-
157
- last = last_entry(opt)
158
- if last.nil?
159
- logger.warn('Skipped:', 'No previous entry found')
160
- return
161
- end
138
+ # @content.update_item(original, item)
139
+ add_item(title, section, { note: note, back: opt[:date], timed: false })
140
+ end
162
141
 
163
- repeat_item(last, opt)
164
- write(@doing_file)
142
+ ##
143
+ ## Restart the last entry
144
+ ##
145
+ ## @param opt [Hash] Additional Options
146
+ ##
147
+ def repeat_last(opt)
148
+ opt ||= {}
149
+ opt[:section] ||= 'all'
150
+ opt[:section] = guess_section(opt[:section])
151
+ opt[:note] ||= []
152
+ opt[:tag] ||= []
153
+ opt[:tag_bool] ||= :and
154
+
155
+ last = last_entry(opt)
156
+ if last.nil?
157
+ logger.warn('Skipped:', 'No previous entry found')
158
+ return
165
159
  end
166
160
 
161
+ repeat_item(last, opt)
162
+ write(@doing_file)
163
+ end
167
164
 
168
- ##
169
- ## Tag the last entry or X entries
170
- ##
171
- ## @param opt [Hash] Additional Options (see
172
- ## #filter_items for filtering
173
- ## options)
174
- ##
175
- ## @see #filter_items
176
- ##
177
- def tag_last(opt) # hooked
178
- opt ||= {}
179
- opt[:count] ||= 1
180
- opt[:archive] ||= false
181
- opt[:tags] ||= ['done']
182
- opt[:sequential] ||= false
183
- opt[:date] ||= false
184
- opt[:remove] ||= false
185
- opt[:update] ||= false
186
- opt[:autotag] ||= false
187
- opt[:back] ||= false
188
- opt[:unfinished] ||= false
189
- opt[:section] = opt[:section] ? guess_section(opt[:section]) : 'All'
190
-
191
- items = filter_items(Items.new, opt: opt)
192
-
193
- if opt[:interactive]
194
- items = Prompt.choose_from_items(items, include_section: opt[:section] =~ /^all$/i, menu: true,
195
- header: '',
196
- prompt: 'Select entries to tag > ',
197
- multiple: true,
198
- sort: true,
199
- show_if_single: true)
200
-
201
- raise NoResults, 'no items selected' if items.empty?
202
165
 
203
- end
166
+ ##
167
+ ## Tag the last entry or X entries
168
+ ##
169
+ ## @param opt [Hash] Additional Options (see
170
+ ## #filter_items for filtering
171
+ ## options)
172
+ ##
173
+ ## @see #filter_items
174
+ ##
175
+ def tag_last(opt) # hooked
176
+ opt ||= {}
177
+ opt[:count] ||= 1
178
+ opt[:archive] ||= false
179
+ opt[:tags] ||= ['done']
180
+ opt[:sequential] ||= false
181
+ opt[:date] ||= false
182
+ opt[:remove] ||= false
183
+ opt[:update] ||= false
184
+ opt[:autotag] ||= false
185
+ opt[:back] ||= false
186
+ opt[:unfinished] ||= false
187
+ opt[:section] = opt[:section] ? guess_section(opt[:section]) : 'All'
188
+
189
+ items = filter_items(Items.new, opt: opt)
190
+
191
+ if opt[:interactive]
192
+ items = Prompt.choose_from_items(items, include_section: opt[:section] =~ /^all$/i, menu: true,
193
+ header: '',
194
+ prompt: 'Select entries to tag > ',
195
+ multiple: true,
196
+ sort: true,
197
+ show_if_single: true)
198
+
199
+ raise NoResults, 'no items selected' if items.empty?
204
200
 
205
- raise NoResults, 'no items matched your search' if items.empty?
201
+ end
206
202
 
207
- if opt[:tags].empty? && !opt[:autotag]
208
- completions = opt[:remove] ? all_tags(items) : all_tags(@content)
209
- if opt[:remove]
210
- puts "#{yellow}Available tags: #{boldwhite}#{completions.map(&:add_at).join(', ')}"
211
- else
212
- puts "#{yellow}Use tab to complete known tags"
213
- end
214
- opt[:tags] = Doing::Prompt.read_line(prompt: "Enter tag(s) to #{opt[:remove] ? 'remove' : 'add'}",
215
- completions: completions,
216
- default_response: '').to_tags
217
- raise UserCancelled, 'No tags provided' if opt[:tags].empty?
203
+ raise NoResults, 'no items matched your search' if items.empty?
204
+
205
+ if opt[:tags].empty? && !opt[:autotag]
206
+ completions = opt[:remove] ? all_tags(items) : all_tags(@content)
207
+ if opt[:remove]
208
+ puts "#{yellow}Available tags: #{boldwhite}#{completions.map(&:add_at).join(', ')}"
209
+ else
210
+ puts "#{yellow}Use tab to complete known tags"
218
211
  end
212
+ opt[:tags] = Doing::Prompt.read_line(prompt: "Enter tag(s) to #{opt[:remove] ? 'remove' : 'add'}",
213
+ completions: completions,
214
+ default_response: '').to_tags
215
+ raise UserCancelled, 'No tags provided' if opt[:tags].empty?
216
+ end
219
217
 
220
- items.each do |item|
221
- old_item = item.clone
222
- added = []
223
- removed = []
218
+ items.each do |item|
219
+ old_item = item.clone
220
+ added = []
221
+ removed = []
224
222
 
225
- item.date = opt[:start_date] if opt[:start_date]
223
+ item.date = opt[:start_date] if opt[:start_date]
226
224
 
227
- if opt[:autotag]
228
- new_title = autotag(item.title) if Doing.auto_tag
229
- if new_title == item.title
230
- logger.count(:skipped, level: :debug, message: '%count unchaged %items')
231
- # logger.debug('Autotag:', 'No changes')
232
- else
233
- logger.count(:added_tags)
234
- logger.write(items.count == 1 ? :info : :debug, 'Tagged:', new_title)
235
- item.title = new_title
236
- end
225
+ if opt[:autotag]
226
+ new_title = autotag(item.title) if Doing.auto_tag
227
+ if new_title == item.title
228
+ logger.count(:skipped, level: :debug, message: '%count unchaged %items')
229
+ # logger.debug('Autotag:', 'No changes')
237
230
  else
238
- if opt[:done_date]
239
- done_date = opt[:done_date]
240
- elsif opt[:sequential]
241
- next_entry = next_item(item)
242
-
243
- done_date = if next_entry.nil?
244
- Time.now
245
- else
246
- next_entry.date - 60
247
- end
248
- else
249
- done_date = item.calculate_end_date(opt)
250
- end
231
+ logger.count(:added_tags)
232
+ logger.write(items.count == 1 ? :info : :debug, 'Tagged:', new_title)
233
+ item.title = new_title
234
+ end
235
+ else
236
+ if opt[:done_date]
237
+ done_date = opt[:done_date]
238
+ elsif opt[:sequential]
239
+ next_entry = next_item(item)
240
+
241
+ done_date = if next_entry.nil?
242
+ Time.now
243
+ else
244
+ next_entry.date - 60
245
+ end
246
+ else
247
+ done_date = item.calculate_end_date(opt)
248
+ end
251
249
 
252
- opt[:tags].each do |tag|
253
- if tag == 'done' && !item.should_finish?
250
+ opt[:tags].each do |tag|
251
+ if tag == 'done' && !item.should_finish?
254
252
 
255
- Doing.logger.debug('Skipped:', "Item in never_finish: #{item.title}")
256
- logger.count(:skipped, level: :debug)
257
- next
258
- end
253
+ Doing.logger.debug('Skipped:', "Item in never_finish: #{item.title}")
254
+ logger.count(:skipped, level: :debug)
255
+ next
256
+ end
259
257
 
260
- tag = tag.strip
258
+ tag = tag.strip
261
259
 
262
- if tag =~ /^(\S+)\((.*?)\)$/
263
- m = Regexp.last_match
264
- tag = m[1]
265
- opt[:value] ||= m[2]
266
- end
260
+ if tag =~ /^(\S+)\((.*?)\)$/
261
+ m = Regexp.last_match
262
+ tag = m[1]
263
+ opt[:value] ||= m[2]
264
+ end
267
265
 
268
- if tag =~ /^done$/ && opt[:date] && item.should_time?
269
- max_elapsed = Doing.setting('interaction.confirm_longer_than', 0)
270
- max_elapsed = max_elapsed.chronify_qty if max_elapsed.is_a?(String)
271
- elapsed = done_date - item.date
272
-
273
- if max_elapsed.positive? && (elapsed > max_elapsed) && !opt[:took]
274
- puts boldwhite(item.title)
275
- human = elapsed.time_string(format: :natural)
276
- res = Prompt.yn(yellow("Did this actually take #{human}"), default_response: true)
277
- unless res
278
- new_elapsed = Prompt.enter_text('How long did it take?').chronify_qty
279
- raise InvalidTimeExpression, 'Unrecognized time span entry' unless new_elapsed > 0
280
-
281
- opt[:took] = new_elapsed
282
- done_date = item.calculate_end_date(opt) if opt[:took]
283
- end
266
+ if tag =~ /^done$/ && opt[:date] && item.should_time?
267
+ max_elapsed = Doing.setting('interaction.confirm_longer_than', 0)
268
+ max_elapsed = max_elapsed.chronify_qty if max_elapsed.is_a?(String)
269
+ elapsed = done_date - item.date
270
+
271
+ if max_elapsed.positive? && (elapsed > max_elapsed) && !opt[:took]
272
+ puts boldwhite(item.title)
273
+ human = elapsed.time_string(format: :natural)
274
+ res = Prompt.yn(yellow("Did this actually take #{human}"), default_response: true)
275
+ unless res
276
+ new_elapsed = Prompt.enter_text('How long did it take?').chronify_qty
277
+ raise InvalidTimeExpression, 'Unrecognized time span entry' unless new_elapsed > 0
278
+
279
+ opt[:took] = new_elapsed
280
+ done_date = item.calculate_end_date(opt) if opt[:took]
284
281
  end
285
282
  end
283
+ end
286
284
 
287
- if opt[:remove] || opt[:rename] || opt[:value]
288
- rename_to = nil
289
- if opt[:value]
290
- rename_to = tag
291
- elsif opt[:rename]
292
- rename_to = tag
293
- tag = opt[:rename]
294
- end
295
- old_title = item.title.dup
296
- force = opt[:value].nil? ? false : true
297
- item.title.tag!(tag, remove: opt[:remove], rename_to: rename_to, regex: opt[:regex], value: opt[:value], force: force)
298
- if old_title != item.title
299
- removed << tag
300
- added << rename_to if rename_to
301
- else
302
- logger.count(:skipped, level: :debug)
303
- end
285
+ if opt[:remove] || opt[:rename] || opt[:value]
286
+ rename_to = nil
287
+ if opt[:value]
288
+ rename_to = tag
289
+ elsif opt[:rename]
290
+ rename_to = tag
291
+ tag = opt[:rename]
292
+ end
293
+ old_title = item.title.dup
294
+ force = opt[:value].nil? ? false : true
295
+ item.title.tag!(tag, remove: opt[:remove], rename_to: rename_to, regex: opt[:regex], value: opt[:value], force: force)
296
+ if old_title != item.title
297
+ removed << tag
298
+ added << rename_to if rename_to
304
299
  else
305
- old_title = item.title.dup
306
- should_date = opt[:date] && item.should_time?
307
- item.title.tag!('done', remove: true) if tag =~ /done/ && (!should_date || opt[:update])
308
- item.title.tag!(tag, value: should_date ? done_date.strftime('%F %R') : nil)
309
- added << tag if old_title != item.title
300
+ logger.count(:skipped, level: :debug)
310
301
  end
302
+ else
303
+ old_title = item.title.dup
304
+ should_date = opt[:date] && item.should_time?
305
+ item.title.tag!('done', remove: true) if tag =~ /done/ && (!should_date || opt[:update])
306
+ item.title.tag!(tag, value: should_date ? done_date.strftime('%F %R') : nil)
307
+ added << tag if old_title != item.title
311
308
  end
312
309
  end
310
+ end
313
311
 
314
- logger.log_change(tags_added: added, tags_removed: removed, item: item, single: items.count == 1)
315
-
316
- item.note.add(opt[:note]) if opt[:note]
312
+ logger.log_change(tags_added: added, tags_removed: removed, item: item, single: items.count == 1)
317
313
 
318
- if opt[:archive] && opt[:section] != 'Archive' && (opt[:count]).positive?
319
- item.move_to('Archive', label: true)
320
- elsif opt[:archive] && opt[:count].zero?
321
- logger.warn('Skipped:', 'Archiving is skipped when operating on all entries')
322
- end
314
+ item.note.add(opt[:note]) if opt[:note]
323
315
 
324
- item.expand_date_tags(Doing.setting('date_tags'))
325
- Hooks.trigger :post_entry_updated, self, item, old_item
316
+ if opt[:archive] && opt[:section] != 'Archive' && (opt[:count]).positive?
317
+ item.move_to('Archive', label: true)
318
+ elsif opt[:archive] && opt[:count].zero?
319
+ logger.warn('Skipped:', 'Archiving is skipped when operating on all entries')
326
320
  end
327
321
 
328
- write(@doing_file)
322
+ item.expand_date_tags(Doing.setting('date_tags'))
323
+ Hooks.trigger :post_entry_updated, self, item, old_item
329
324
  end
330
325
 
331
- ##
332
- ## Accepts one tag and the raw text of a new item if the
333
- ## passed tag is on any item, it's replaced with @done.
334
- ## if new_item is not nil, it's tagged with the passed
335
- ## tag and inserted. This is for use where only one
336
- ## instance of a given tag should exist (@meanwhile)
337
- ##
338
- ## @param target_tag [String] Tag to replace
339
- ## @param opt [Hash] Additional Options
340
- ##
341
- ## @option opt :section [String] target section
342
- ## @option opt :archive [Boolean] archive old item
343
- ## @option opt :back [Date] backdate new item
344
- ## @option opt :new_item [String] content to use for new item
345
- ## @option opt :note [Array] note content for new item
346
- def stop_start(target_tag, opt)
347
- opt ||= {}
348
- tag = target_tag.dup
349
- opt[:section] ||= Doing.setting('current_section')
350
- opt[:archive] ||= false
351
- opt[:back] ||= Time.now
352
- opt[:new_item] ||= false
353
- opt[:note] ||= false
354
-
355
- opt[:section] = guess_section(opt[:section])
356
-
357
- tag.sub!(/^@/, '')
358
-
359
- found_items = 0
360
-
361
- @content.each_with_index do |item, i|
362
- old_item = i.clone
363
- next unless item.section == opt[:section] || opt[:section] =~ /all/i
364
-
365
- next unless item.title =~ /@#{tag}/
366
-
367
- item.title.add_tags!([tag, 'done'], remove: true)
368
- item.tag('done', value: opt[:back].strftime('%F %R'))
369
-
370
- found_items += 1
371
-
372
- if opt[:archive] && opt[:section] != 'Archive'
373
- item.title = item.title.sub(/(?:@from\(.*?\))?(.*)$/, "\\1 @from(#{item.section})")
374
- item.move_to('Archive', label: false, log: false)
375
- logger.count(:completed_archived)
376
- logger.info('Completed/archived:', item.title)
377
- else
378
- logger.count(:completed)
379
- logger.info('Completed:', item.title)
380
- end
381
- Hooks.trigger :post_entry_updated, self, item, old_item
382
- end
326
+ write(@doing_file)
327
+ end
383
328
 
329
+ ##
330
+ ## Accepts one tag and the raw text of a new item if the
331
+ ## passed tag is on any item, it's replaced with @done.
332
+ ## if new_item is not nil, it's tagged with the passed
333
+ ## tag and inserted. This is for use where only one
334
+ ## instance of a given tag should exist (@meanwhile)
335
+ ##
336
+ ## @param target_tag [String] Tag to replace
337
+ ## @param opt [Hash] Additional Options
338
+ ##
339
+ ## @option opt :section [String] target section
340
+ ## @option opt :archive [Boolean] archive old item
341
+ ## @option opt :back [Date] backdate new item
342
+ ## @option opt :new_item [String] content to use for new item
343
+ ## @option opt :note [Array] note content for new item
344
+ def stop_start(target_tag, opt)
345
+ opt ||= {}
346
+ tag = target_tag.dup
347
+ opt[:section] ||= Doing.setting('current_section')
348
+ opt[:archive] ||= false
349
+ opt[:back] ||= Time.now
350
+ opt[:new_item] ||= false
351
+ opt[:note] ||= false
352
+
353
+ opt[:section] = guess_section(opt[:section])
354
+
355
+ tag.sub!(/^@/, '')
356
+
357
+ found_items = 0
358
+
359
+ @content.each_with_index do |item, i|
360
+ old_item = i.clone
361
+ next unless item.section == opt[:section] || opt[:section] =~ /all/i
362
+
363
+ next unless item.title =~ /@#{tag}/
364
+
365
+ item.title.add_tags!([tag, 'done'], remove: true)
366
+ item.tag('done', value: opt[:back].strftime('%F %R'))
367
+
368
+ found_items += 1
369
+
370
+ if opt[:archive] && opt[:section] != 'Archive'
371
+ item.title = item.title.sub(/(?:@from\(.*?\))?(.*)$/, "\\1 @from(#{item.section})")
372
+ item.move_to('Archive', label: false, log: false)
373
+ logger.count(:completed_archived)
374
+ logger.info('Completed/archived:', item.title)
375
+ else
376
+ logger.count(:completed)
377
+ logger.info('Completed:', item.title)
378
+ end
379
+ Hooks.trigger :post_entry_updated, self, item, old_item
380
+ end
384
381
 
385
- logger.debug('Skipped:', "No active @#{tag} tasks found.") if found_items.zero?
386
382
 
387
- if opt[:new_item]
388
- date, title, note = format_input(opt[:new_item])
389
- opt[:back] = date unless date.nil?
390
- note.add(opt[:note]) if opt[:note]
391
- title.tag!(tag)
392
- add_item(title.cap_first, opt[:section], { note: note, back: opt[:back] })
393
- end
383
+ logger.debug('Skipped:', "No active @#{tag} tasks found.") if found_items.zero?
394
384
 
395
- write(@doing_file)
385
+ if opt[:new_item]
386
+ date, title, note = format_input(opt[:new_item])
387
+ opt[:back] = date unless date.nil?
388
+ note.add(opt[:note]) if opt[:note]
389
+ title.tag!(tag)
390
+ add_item(title.cap_first, opt[:section], { note: note, back: opt[:back] })
396
391
  end
397
392
 
398
- ##
399
- ## Delete a set of items from the main index
400
- ##
401
- ## @param items [Array] The items to delete
402
- ## @param force [Boolean] Force deletion without confirmation
403
- ##
404
- def delete_items(items, force: false)
405
- items.slice(0, 5).each { |i| puts i.to_pretty } unless force
406
- puts softpurple("+ #{items.size - 5} additional #{'item'.to_p(items.size - 5)}") if items.size > 5 && !force
407
-
408
- res = force ? true : Prompt.yn("Delete #{items.size} #{'item'.to_p(items.size)}?", default_response: 'y')
409
- return unless res
410
-
411
- items.each { |i| Hooks.trigger :post_entry_removed, self, @content.delete_item(i, single: items.count == 1) }
412
- # write(@doing_file)
393
+ write(@doing_file)
394
+ end
395
+
396
+ ##
397
+ ## Delete a set of items from the main index
398
+ ##
399
+ ## @param items [Array] The items to delete
400
+ ## @param force [Boolean] Force deletion without confirmation
401
+ ##
402
+ def delete_items(items, force: false)
403
+ items.slice(0, 5).each { |i| puts i.to_pretty } unless force
404
+ puts softpurple("+ #{items.size - 5} additional #{'item'.to_p(items.size - 5)}") if items.size > 5 && !force
405
+
406
+ res = force ? true : Prompt.yn("Delete #{items.size} #{'item'.to_p(items.size)}?", default_response: 'y')
407
+ return unless res
408
+
409
+ items.each { |i| Hooks.trigger :post_entry_removed, self, @content.delete_item(i, single: items.count == 1) }
410
+ # write(@doing_file)
411
+ end
412
+
413
+ ##
414
+ ## Move entries from a section to Archive or other specified
415
+ ## section
416
+ ##
417
+ ## @param section [String] The source section
418
+ ## @param options [Hash] Options
419
+ ##
420
+ def archive(section = Doing.setting('current_section'), options)
421
+ options ||= {}
422
+ count = options[:keep] || 0
423
+ destination = options[:destination] || 'Archive'
424
+ tags = options[:tags] || []
425
+ bool = options[:bool] || :and
426
+
427
+ section = choose_section if section.nil? || section =~ /choose/i
428
+ archive_all = section =~ /^all$/i # && !(tags.nil? || tags.empty?)
429
+ section = guess_section(section) unless archive_all
430
+
431
+ @content.add_section(destination, log: true)
432
+ # add_section(Section.new('Archive')) if destination =~ /^archive$/i && !@content.section?('Archive')
433
+
434
+ destination = guess_section(destination)
435
+
436
+ if @content.section?(destination) && (@content.section?(section) || archive_all)
437
+ do_archive(section, destination, { count: count, tags: tags, bool: bool, search: options[:search], label: options[:label], before: options[:before], after: options[:after], from: options[:from] })
438
+ write(doing_file)
439
+ else
440
+ raise InvalidArgument, 'Either source or destination does not exist'
413
441
  end
442
+ end
414
443
 
415
- ##
416
- ## Move entries from a section to Archive or other specified
417
- ## section
418
- ##
419
- ## @param section [String] The source section
420
- ## @param options [Hash] Options
421
- ##
422
- def archive(section = Doing.setting('current_section'), options)
423
- options ||= {}
424
- count = options[:keep] || 0
425
- destination = options[:destination] || 'Archive'
426
- tags = options[:tags] || []
427
- bool = options[:bool] || :and
428
-
429
- section = choose_section if section.nil? || section =~ /choose/i
430
- archive_all = section =~ /^all$/i # && !(tags.nil? || tags.empty?)
431
- section = guess_section(section) unless archive_all
432
-
433
- @content.add_section(destination, log: true)
434
- # add_section(Section.new('Archive')) if destination =~ /^archive$/i && !@content.section?('Archive')
435
-
436
- destination = guess_section(destination)
437
-
438
- if @content.section?(destination) && (@content.section?(section) || archive_all)
439
- do_archive(section, destination, { count: count, tags: tags, bool: bool, search: options[:search], label: options[:label], before: options[:before], after: options[:after], from: options[:from] })
440
- write(doing_file)
441
- else
442
- raise InvalidArgument, 'Either source or destination does not exist'
444
+ ##
445
+ ## Uses 'autotag' configuration to turn keywords into tags for time tracking.
446
+ ## Does not repeat tags in a title, and only converts the first instance of an
447
+ ## untagged keyword
448
+ ##
449
+ ## @param string [String] The text to tag
450
+ ##
451
+ def autotag(string)
452
+ return unless string
453
+ return string unless Doing.auto_tag
454
+
455
+ original = string.dup
456
+ text = string.dup
457
+
458
+ current_tags = text.scan(/@\w+/).map { |t| t.sub(/^@/, '') }
459
+ tagged = {
460
+ whitelisted: [],
461
+ synonyms: [],
462
+ transformed: [],
463
+ replaced: []
464
+ }
465
+
466
+ Doing.setting('autotag.whitelist').each do |tag|
467
+ next if text =~ /@#{tag}\b/i
468
+
469
+ text.sub!(/(?<= |\A)(#{tag.strip})(?= |\Z)/i) do |m|
470
+ m.downcase! unless tag =~ /[A-Z]/
471
+ tagged[:whitelisted].push(m)
472
+ "@#{m}"
443
473
  end
444
474
  end
445
475
 
446
- ##
447
- ## Uses 'autotag' configuration to turn keywords into tags for time tracking.
448
- ## Does not repeat tags in a title, and only converts the first instance of an
449
- ## untagged keyword
450
- ##
451
- ## @param string [String] The text to tag
452
- ##
453
- def autotag(string)
454
- return unless string
455
- return string unless Doing.auto_tag
456
-
457
- original = string.dup
458
- text = string.dup
459
-
460
- current_tags = text.scan(/@\w+/).map { |t| t.sub(/^@/, '') }
461
- tagged = {
462
- whitelisted: [],
463
- synonyms: [],
464
- transformed: [],
465
- replaced: []
466
- }
467
-
468
- Doing.setting('autotag.whitelist').each do |tag|
469
- next if text =~ /@#{tag}\b/i
470
-
471
- text.sub!(/(?<= |\A)(#{tag.strip})(?= |\Z)/i) do |m|
472
- m.downcase! unless tag =~ /[A-Z]/
473
- tagged[:whitelisted].push(m)
474
- "@#{m}"
476
+ Doing.setting('autotag.synonyms').each do |tag, v|
477
+ v.each do |word|
478
+ word = word.wildcard_to_rx
479
+ next unless text =~ /\b#{word}\b/i
480
+
481
+ unless current_tags.include?(tag) || tagged[:whitelisted].include?(tag)
482
+ tagged[:synonyms].push(tag)
483
+ tagged[:synonyms] = tagged[:synonyms].uniq
475
484
  end
476
485
  end
486
+ end
477
487
 
478
- Doing.setting('autotag.synonyms').each do |tag, v|
479
- v.each do |word|
480
- word = word.wildcard_to_rx
481
- next unless text =~ /\b#{word}\b/i
488
+ if Doing.setting('autotag.transform')
489
+ Doing.setting('autotag.transform').each do |tag|
490
+ next unless tag =~ /\S+:\S+/
482
491
 
483
- unless current_tags.include?(tag) || tagged[:whitelisted].include?(tag)
484
- tagged[:synonyms].push(tag)
485
- tagged[:synonyms] = tagged[:synonyms].uniq
486
- end
492
+ if tag =~ /::/
493
+ rx, r = tag.split(/::/)
494
+ else
495
+ rx, r = tag.split(/:/)
487
496
  end
488
- end
489
497
 
490
- if Doing.setting('autotag.transform')
491
- Doing.setting('autotag.transform').each do |tag|
492
- next unless tag =~ /\S+:\S+/
498
+ flag_rx = %r{/([r]+)$}
499
+ if r =~ flag_rx
500
+ flags = r.match(flag_rx)[1].split(//)
501
+ r.sub!(flag_rx, '')
502
+ end
503
+ r.gsub!(/\$/, '\\')
504
+ rx.sub!(/^@?/, '@')
505
+ regex = Regexp.new("(?<= |\\A)#{rx}(?= |\\Z)")
493
506
 
494
- if tag =~ /::/
495
- rx, r = tag.split(/::/)
496
- else
497
- rx, r = tag.split(/:/)
498
- end
507
+ text.sub!(regex) do
508
+ m = Regexp.last_match
509
+ new_tag = r
499
510
 
500
- flag_rx = %r{/([r]+)$}
501
- if r =~ flag_rx
502
- flags = r.match(flag_rx)[1].split(//)
503
- r.sub!(flag_rx, '')
511
+ m.to_a.slice(1, m.length - 1).each_with_index do |v, idx|
512
+ new_tag.gsub!("\\#{idx + 1}", v)
504
513
  end
505
- r.gsub!(/\$/, '\\')
506
- rx.sub!(/^@?/, '@')
507
- regex = Regexp.new("(?<= |\\A)#{rx}(?= |\\Z)")
508
-
509
- text.sub!(regex) do
510
- m = Regexp.last_match
511
- new_tag = r
512
-
513
- m.to_a.slice(1, m.length - 1).each_with_index do |v, idx|
514
- new_tag.gsub!("\\#{idx + 1}", v)
515
- end
516
- # Replace original tag if /r
517
- if flags&.include?('r')
518
- tagged[:replaced].concat(new_tag.split(/ /).map { |t| t.sub(/^@/, '') })
519
- new_tag.split(/ /).map { |t| t.sub(/^@?/, '@') }.join(' ')
520
- else
521
- tagged[:transformed].concat(new_tag.split(/ /).map { |t| t.sub(/^@/, '') })
522
- tagged[:transformed] = tagged[:transformed].uniq
523
- m[0]
524
- end
514
+ # Replace original tag if /r
515
+ if flags&.include?('r')
516
+ tagged[:replaced].concat(new_tag.split(/ /).map { |t| t.sub(/^@/, '') })
517
+ new_tag.split(/ /).map { |t| t.sub(/^@?/, '@') }.join(' ')
518
+ else
519
+ tagged[:transformed].concat(new_tag.split(/ /).map { |t| t.sub(/^@/, '') })
520
+ tagged[:transformed] = tagged[:transformed].uniq
521
+ m[0]
525
522
  end
526
523
  end
527
524
  end
525
+ end
528
526
 
529
- logger.debug('Autotag:', "whitelisted tags: #{tagged[:whitelisted].log_tags}") unless tagged[:whitelisted].empty?
530
- logger.debug('Autotag:', "synonyms: #{tagged[:synonyms].log_tags}") unless tagged[:synonyms].empty?
531
- logger.debug('Autotag:', "transforms: #{tagged[:transformed].log_tags}") unless tagged[:transformed].empty?
532
- logger.debug('Autotag:', "transform replaced: #{tagged[:replaced].log_tags}") unless tagged[:replaced].empty?
533
-
534
- tail_tags = tagged[:synonyms].concat(tagged[:transformed])
535
- tail_tags.sort!
536
- tail_tags.uniq!
527
+ logger.debug('Autotag:', "whitelisted tags: #{tagged[:whitelisted].log_tags}") unless tagged[:whitelisted].empty?
528
+ logger.debug('Autotag:', "synonyms: #{tagged[:synonyms].log_tags}") unless tagged[:synonyms].empty?
529
+ logger.debug('Autotag:', "transforms: #{tagged[:transformed].log_tags}") unless tagged[:transformed].empty?
530
+ logger.debug('Autotag:', "transform replaced: #{tagged[:replaced].log_tags}") unless tagged[:replaced].empty?
537
531
 
538
- text.add_tags!(tail_tags) unless tail_tags.empty?
532
+ tail_tags = tagged[:synonyms].concat(tagged[:transformed])
533
+ tail_tags.sort!
534
+ tail_tags.uniq!
539
535
 
540
- if text == original
541
- logger.debug('Autotag:', "no change to \"#{text.strip}\"")
542
- else
543
- new_tags = tagged[:whitelisted].concat(tail_tags).concat(tagged[:replaced])
544
- logger.debug('Autotag:', "added #{new_tags.log_tags} to \"#{text.strip}\"")
545
- logger.count(:autotag, level: :info, count: 1, message: 'autotag updated %count %items')
546
- end
536
+ text.add_tags!(tail_tags) unless tail_tags.empty?
547
537
 
548
- text.dedup_tags
538
+ if text == original
539
+ logger.debug('Autotag:', "no change to \"#{text.strip}\"")
540
+ else
541
+ new_tags = tagged[:whitelisted].concat(tail_tags).concat(tagged[:replaced])
542
+ logger.debug('Autotag:', "added #{new_tags.log_tags} to \"#{text.strip}\"")
543
+ logger.count(:autotag, level: :info, count: 1, message: 'autotag updated %count %items')
549
544
  end
550
545
 
551
- private
546
+ text.dedup_tags
547
+ end
552
548
 
553
- ##
554
- ## Helper function, performs the actual archiving
555
- ##
556
- ## @param section [String] The source section
557
- ## @param destination [String] The destination
558
- ## section
559
- ## @param opt [Hash] Additional Options
560
- ## @api private
561
- def do_archive(section, destination, opt)
562
- opt ||= {}
563
- count = opt[:count] || 0
564
- tags = opt[:tags] || []
565
- bool = opt[:bool] || :and
566
- label = opt[:label] || true
549
+ private
567
550
 
568
- section = guess_section(section)
569
- destination = guess_section(destination)
551
+ ##
552
+ ## Helper function, performs the actual archiving
553
+ ##
554
+ ## @param section [String] The source section
555
+ ## @param destination [String] The destination
556
+ ## section
557
+ ## @param opt [Hash] Additional Options
558
+ ## @api private
559
+ def do_archive(section, destination, opt)
560
+ opt ||= {}
561
+ count = opt[:count] || 0
562
+ tags = opt[:tags] || []
563
+ bool = opt[:bool] || :and
564
+ label = opt[:label] || true
570
565
 
571
- section_items = @content.in_section(section)
572
- max = section_items.count - count.to_i
566
+ section = guess_section(section)
567
+ destination = guess_section(destination)
573
568
 
574
- opt[:after] = opt[:from][0] if opt[:from]
575
- opt[:before] = opt[:from][1] if opt[:from]
569
+ section_items = @content.in_section(section)
570
+ max = section_items.count - count.to_i
576
571
 
577
- time_rx = /^(\d{1,2}+(:\d{1,2}+)?( *(am|pm))?|midnight|noon)$/
572
+ opt[:after] = opt[:from][0] if opt[:from]
573
+ opt[:before] = opt[:from][1] if opt[:from]
578
574
 
579
- if opt[:before].is_a?(String) && opt[:before] =~ time_rx
580
- opt[:before] = opt[:before].chronify(guess: :end, future: false)
581
- end
575
+ time_rx = /^(\d{1,2}+(:\d{1,2}+)?( *(am|pm))?|midnight|noon)$/
582
576
 
583
- if opt[:after].is_a?(String) && opt[:after] =~ time_rx
584
- opt[:after] = opt[:after].chronify(guess: :begin, future: false)
585
- end
577
+ if opt[:before].is_a?(String) && opt[:before] =~ time_rx
578
+ opt[:before] = opt[:before].chronify(guess: :end, future: false)
579
+ end
586
580
 
587
- counter = 0
581
+ if opt[:after].is_a?(String) && opt[:after] =~ time_rx
582
+ opt[:after] = opt[:after].chronify(guess: :begin, future: false)
583
+ end
588
584
 
589
- @content.map do |item|
590
- break if counter >= max
585
+ counter = 0
591
586
 
592
- next if item.section.downcase == destination.downcase
587
+ @content.map do |item|
588
+ break if counter >= max
593
589
 
594
- next if item.section.downcase != section.downcase && section != /^all$/i
590
+ next if item.section.downcase == destination.downcase
595
591
 
596
- next if (opt[:before] && item.date > opt[:before]) || (opt[:after] && item.date < opt[:after])
592
+ next if item.section.downcase != section.downcase && section != /^all$/i
597
593
 
598
- next if (!tags.empty? && !item.tags?(tags, bool)) || (opt[:search] && !item.search(opt[:search].to_s))
594
+ next if (opt[:before] && item.date > opt[:before]) || (opt[:after] && item.date < opt[:after])
599
595
 
600
- counter += 1
601
- old_item = item.clone
602
- item.move_to(destination, label: label, log: false)
603
- Hooks.trigger :post_entry_updated, self, item, old_item
604
- item
605
- end
596
+ next if (!tags.empty? && !item.tags?(tags, bool)) || (opt[:search] && !item.search(opt[:search].to_s))
606
597
 
607
- if counter.positive?
608
- logger.count(destination == 'Archive' ? :archived : :moved,
609
- level: :info,
610
- count: counter,
611
- message: "%count %items from #{section} to #{destination}")
612
- else
613
- logger.info('Skipped:', 'No items were moved')
614
- end
598
+ counter += 1
599
+ old_item = item.clone
600
+ item.move_to(destination, label: label, log: false)
601
+ Hooks.trigger :post_entry_updated, self, item, old_item
602
+ item
603
+ end
604
+
605
+ if counter.positive?
606
+ logger.count(destination == 'Archive' ? :archived : :moved,
607
+ level: :info,
608
+ count: counter,
609
+ message: "%count %items from #{section} to #{destination}")
610
+ else
611
+ logger.info('Skipped:', 'No items were moved')
615
612
  end
616
613
  end
617
614
  end