doing 2.1.40 → 2.1.41
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.yardopts +1 -1
- data/CHANGELOG.md +22 -0
- data/Gemfile.lock +1 -1
- data/Rakefile +4 -4
- data/bin/commands/changes.rb +1 -1
- data/bin/commands/tag_dir.rb +49 -15
- data/{Dockerfile → docker/Dockerfile} +3 -1
- data/{Dockerfile-2.6 → docker/Dockerfile-2.6} +2 -2
- data/{Dockerfile-2.7 → docker/Dockerfile-2.7} +2 -2
- data/{Dockerfile-3.0 → docker/Dockerfile-3.0} +2 -2
- data/{bash_profile → docker/bash_profile} +0 -0
- data/{inputrc → docker/inputrc} +0 -0
- data/docs/doc/Array.html +84 -2
- data/docs/doc/BooleanTermParser/Clause.html +1 -1
- data/docs/doc/BooleanTermParser/Operator.html +1 -1
- data/docs/doc/BooleanTermParser/Query.html +1 -1
- data/docs/doc/BooleanTermParser/QueryParser.html +1 -1
- data/docs/doc/BooleanTermParser/QueryTransformer.html +1 -1
- data/docs/doc/BooleanTermParser.html +1 -1
- data/docs/doc/Doing/ArrayNestedHash.html +198 -0
- data/docs/doc/Doing/ArrayTags.html +424 -0
- data/docs/doc/Doing/CSVExport.html +266 -0
- data/docs/doc/Doing/CalendarImport.html +232 -0
- data/docs/doc/Doing/Change.html +617 -0
- data/docs/doc/Doing/Changes.html +468 -0
- data/docs/doc/Doing/ChronifyArray.html +347 -0
- data/docs/doc/Doing/ChronifyNumeric.html +271 -0
- data/docs/doc/Doing/ChronifyString.html +682 -0
- data/docs/doc/Doing/Color.html +2 -2
- data/docs/doc/Doing/Completion/BashCompletions.html +445 -0
- data/docs/doc/Doing/Completion/FishCompletions.html +445 -0
- data/docs/doc/Doing/Completion/StringUtils.html +229 -0
- data/docs/doc/Doing/Completion/ZshCompletions.html +445 -0
- data/docs/doc/Doing/Completion.html +17 -3
- data/docs/doc/Doing/Configuration.html +1 -1
- data/docs/doc/Doing/DayOneRenderer.html +383 -0
- data/docs/doc/Doing/DayoneExport.html +290 -0
- data/docs/doc/Doing/DoingImport.html +391 -0
- data/docs/doc/Doing/Entry.html +381 -0
- data/docs/doc/Doing/Errors/DoingNoTraceError.html +1 -1
- data/docs/doc/Doing/Errors/DoingRuntimeError.html +1 -1
- data/docs/doc/Doing/Errors/DoingStandardError.html +1 -1
- data/docs/doc/Doing/Errors/EmptyInput.html +1 -1
- data/docs/doc/Doing/Errors/HistoryLimitError.html +1 -1
- data/docs/doc/Doing/Errors/InvalidPlugin.html +1 -1
- data/docs/doc/Doing/Errors/MissingBackupFile.html +1 -1
- data/docs/doc/Doing/Errors/NoResults.html +1 -1
- data/docs/doc/Doing/Errors/PluginException.html +1 -1
- data/docs/doc/Doing/Errors/UserCancelled.html +1 -1
- data/docs/doc/Doing/Errors/WrongCommand.html +1 -1
- data/docs/doc/Doing/Errors.html +1 -1
- data/docs/doc/Doing/HTMLExport.html +256 -0
- data/docs/doc/Doing/Hooks.html +1 -1
- data/docs/doc/Doing/Item.html +47 -3
- data/docs/doc/Doing/ItemDates.html +564 -0
- data/docs/doc/Doing/ItemQuery.html +614 -0
- data/docs/doc/Doing/ItemState.html +387 -0
- data/docs/doc/Doing/ItemTags.html +498 -0
- data/docs/doc/Doing/Items.html +460 -11
- data/docs/doc/Doing/JSONExport.html +222 -0
- data/docs/doc/Doing/Logger.html +1 -1
- data/docs/doc/Doing/MarkdownExport.html +266 -0
- data/docs/doc/Doing/MarkdownRenderer.html +383 -0
- data/docs/doc/Doing/Note.html +16 -3
- data/docs/doc/Doing/Pager.html +1 -1
- data/docs/doc/Doing/Plugins.html +1 -1
- data/docs/doc/Doing/Prompt.html +31 -682
- data/docs/doc/Doing/PromptChoose.html +484 -0
- data/docs/doc/Doing/PromptFZF.html +391 -0
- data/docs/doc/Doing/PromptInput.html +572 -0
- data/docs/doc/Doing/PromptSTD.html +293 -0
- data/docs/doc/Doing/PromptYN.html +237 -0
- data/docs/doc/Doing/Section.html +58 -2
- data/docs/doc/Doing/StringHighlight.html +533 -0
- data/docs/doc/Doing/StringNormalize.html +929 -0
- data/docs/doc/Doing/StringQuery.html +725 -0
- data/docs/doc/Doing/StringTags.html +884 -0
- data/docs/doc/Doing/StringTransform.html +565 -0
- data/docs/doc/Doing/StringTruncate.html +448 -0
- data/docs/doc/Doing/StringURL.html +409 -0
- data/docs/doc/Doing/SymbolNormalize.html +341 -0
- data/docs/doc/Doing/TaskPaperExport.html +222 -0
- data/docs/doc/Doing/TemplateExport.html +249 -0
- data/docs/doc/Doing/TemplateString.html +101 -2
- data/docs/doc/Doing/TimingImport.html +285 -0
- data/docs/doc/Doing/Types.html +1 -1
- data/docs/doc/Doing/Util/Backup.html +9 -7
- data/docs/doc/Doing/Util.html +2 -2
- data/docs/doc/Doing/Version.html +523 -0
- data/docs/doc/Doing/WWID/WWIDUtil.html +510 -0
- data/docs/doc/Doing/WWID.html +4377 -217
- data/docs/doc/Doing/WWIDDisplay.html +865 -0
- data/docs/doc/Doing/WWIDEditor.html +466 -0
- data/docs/doc/Doing/WWIDFileTools.html +359 -0
- data/docs/doc/Doing/WWIDFilter.html +466 -0
- data/docs/doc/Doing/WWIDGuess.html +299 -0
- data/docs/doc/Doing/WWIDInteractive.html +752 -0
- data/docs/doc/Doing/WWIDModify.html +1078 -0
- data/docs/doc/Doing/WWIDTags.html +302 -0
- data/docs/doc/Doing/WWIDTimers.html +359 -0
- data/docs/doc/Doing/WWIDUtil.html +510 -0
- data/docs/doc/Doing.html +9 -6
- data/docs/doc/FalseClass.html +1 -1
- data/docs/doc/GLI/Commands/Help.html +1 -1
- data/docs/doc/GLI/Commands/MarkdownDocumentListener.html +1 -1
- data/docs/doc/GLI/Commands.html +1 -1
- data/docs/doc/GLI.html +1 -1
- data/docs/doc/Hash.html +1 -1
- data/docs/doc/Numeric.html +23 -78
- data/docs/doc/Object.html +1 -1
- data/docs/doc/PhraseParser/Operator.html +1 -1
- data/docs/doc/PhraseParser/PhraseClause.html +1 -1
- data/docs/doc/PhraseParser/Query.html +1 -1
- data/docs/doc/PhraseParser/QueryParser.html +1 -1
- data/docs/doc/PhraseParser/QueryTransformer.html +1 -1
- data/docs/doc/PhraseParser/TermClause.html +1 -1
- data/docs/doc/PhraseParser.html +1 -1
- data/docs/doc/Status.html +1 -1
- data/docs/doc/String.html +58 -633
- data/docs/doc/Symbol.html +9 -224
- data/docs/doc/Time.html +119 -13
- data/docs/doc/TrueClass.html +1 -1
- data/docs/doc/_index.html +324 -8
- data/docs/doc/class_list.html +1 -1
- data/docs/doc/file.README.html +1 -1
- data/docs/doc/index.html +1 -1
- data/docs/doc/method_list.html +2326 -542
- data/docs/doc/top-level-namespace.html +2 -2
- data/doing.rdoc +13 -3
- data/lib/completion/_doing.zsh +1 -1
- data/lib/completion/doing.bash +2 -2
- data/lib/completion/doing.fish +3 -1
- data/lib/doing/array/array.rb +16 -12
- data/lib/doing/array/nested_hash.rb +1 -1
- data/lib/doing/array/tags.rb +6 -5
- data/lib/doing/changelog/changelog.rb +6 -0
- data/lib/doing/chronify/array.rb +1 -3
- data/lib/doing/chronify/chronify.rb +12 -0
- data/lib/doing/chronify/numeric.rb +3 -2
- data/lib/doing/chronify/string.rb +1 -1
- data/lib/doing/completion/completion_string.rb +25 -0
- data/lib/doing/completion.rb +1 -1
- data/lib/doing/good.rb +8 -0
- data/lib/doing/item/dates.rb +1 -1
- data/lib/doing/{item.rb → item/item.rb} +10 -5
- data/lib/doing/item/query.rb +1 -1
- data/lib/doing/item/state.rb +1 -1
- data/lib/doing/item/tags.rb +1 -1
- data/lib/doing/items/filter.rb +67 -0
- data/lib/doing/items/items.rb +57 -0
- data/lib/doing/items/modify.rb +36 -0
- data/lib/doing/items/sections.rb +83 -0
- data/lib/doing/items/util.rb +74 -0
- data/lib/doing/normalize.rb +10 -2
- data/lib/doing/plugins/export/markdown_export.rb +4 -2
- data/lib/doing/plugins/import/doing_import.rb +1 -1
- data/lib/doing/prompt/choose.rb +118 -0
- data/lib/doing/prompt/fzf.rb +84 -0
- data/lib/doing/prompt/input.rb +129 -0
- data/lib/doing/prompt/prompt.rb +41 -0
- data/lib/doing/prompt/std.rb +32 -0
- data/lib/doing/prompt/yn.rb +64 -0
- data/lib/doing/section.rb +4 -0
- data/lib/doing/string/highlight.rb +1 -1
- data/lib/doing/string/query.rb +1 -1
- data/lib/doing/string/string.rb +18 -7
- data/lib/doing/string/tags.rb +14 -3
- data/lib/doing/string/transform.rb +1 -1
- data/lib/doing/string/truncate.rb +1 -1
- data/lib/doing/string/url.rb +1 -1
- data/lib/doing/time.rb +19 -1
- data/lib/doing/util_backup.rb +2 -2
- data/lib/doing/version.rb +1 -1
- data/lib/doing/wwid/display.rb +357 -360
- data/lib/doing/wwid/editor.rb +173 -176
- data/lib/doing/wwid/filetools.rb +156 -159
- data/lib/doing/wwid/filter.rb +191 -183
- data/lib/doing/wwid/guess.rb +58 -60
- data/lib/doing/wwid/interactive.rb +332 -330
- data/lib/doing/wwid/modify.rb +509 -512
- data/lib/doing/wwid/tags.rb +38 -41
- data/lib/doing/wwid/timers.rb +293 -296
- data/lib/doing/{wwid.rb → wwid/wwid.rb} +32 -23
- data/lib/doing/wwid/wwidutil.rb +79 -82
- data/lib/doing.rb +5 -5
- data/lib/helpers/threaded_tests.rb +1 -0
- metadata +76 -14
- data/lib/doing/changelog.rb +0 -6
- data/lib/doing/completion/string.rb +0 -17
- data/lib/doing/items.rb +0 -221
- data/lib/doing/prompt.rb +0 -330
data/lib/doing/wwid/modify.rb
CHANGED
@@ -2,616 +2,613 @@
|
|
2
2
|
|
3
3
|
module Doing
|
4
4
|
class WWID
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
if Doing.
|
35
|
-
|
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
|
-
|
40
|
-
|
37
|
+
title.compress!
|
38
|
+
entry = Item.new(opt[:back], title.strip, section)
|
41
39
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
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
|
-
|
48
|
+
entry.note = note
|
51
49
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
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
|
-
|
59
|
-
|
60
|
-
|
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
|
-
|
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
|
-
|
67
|
-
|
68
|
-
|
68
|
+
Hooks.trigger :post_entry_added, self, entry
|
69
|
+
entry
|
70
|
+
end
|
69
71
|
|
70
|
-
|
71
|
-
|
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
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
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'
|
111
|
+
item.title.tag!('done')
|
88
112
|
end
|
89
|
-
|
90
|
-
item
|
113
|
+
Hooks.trigger :post_entry_updated, self, item, old_item
|
91
114
|
end
|
92
115
|
|
93
|
-
#
|
94
|
-
|
95
|
-
|
96
|
-
|
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
|
-
|
121
|
+
note = opt[:note] || Note.new
|
124
122
|
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
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
|
-
|
130
|
+
opt[:date] = date unless date.nil?
|
133
131
|
|
134
|
-
|
135
|
-
|
136
|
-
|
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
|
-
|
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
|
-
|
164
|
-
|
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
|
-
|
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
|
-
|
201
|
+
end
|
206
202
|
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
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
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
218
|
+
items.each do |item|
|
219
|
+
old_item = item.clone
|
220
|
+
added = []
|
221
|
+
removed = []
|
224
222
|
|
225
|
-
|
223
|
+
item.date = opt[:start_date] if opt[:start_date]
|
226
224
|
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
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
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
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
|
-
|
253
|
-
|
250
|
+
opt[:tags].each do |tag|
|
251
|
+
if tag == 'done' && !item.should_finish?
|
254
252
|
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
253
|
+
Doing.logger.debug('Skipped:', "Item in never_finish: #{item.title}")
|
254
|
+
logger.count(:skipped, level: :debug)
|
255
|
+
next
|
256
|
+
end
|
259
257
|
|
260
|
-
|
258
|
+
tag = tag.strip
|
261
259
|
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
260
|
+
if tag =~ /^(\S+)\((.*?)\)$/
|
261
|
+
m = Regexp.last_match
|
262
|
+
tag = m[1]
|
263
|
+
opt[:value] ||= m[2]
|
264
|
+
end
|
267
265
|
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
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
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
325
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
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
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
if
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
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
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
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
|
-
|
479
|
-
|
480
|
-
|
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
|
-
|
484
|
-
|
485
|
-
|
486
|
-
|
492
|
+
if tag =~ /::/
|
493
|
+
rx, r = tag.split(/::/)
|
494
|
+
else
|
495
|
+
rx, r = tag.split(/:/)
|
487
496
|
end
|
488
|
-
end
|
489
497
|
|
490
|
-
|
491
|
-
|
492
|
-
|
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
|
-
|
495
|
-
|
496
|
-
|
497
|
-
rx, r = tag.split(/:/)
|
498
|
-
end
|
507
|
+
text.sub!(regex) do
|
508
|
+
m = Regexp.last_match
|
509
|
+
new_tag = r
|
499
510
|
|
500
|
-
|
501
|
-
|
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
|
506
|
-
|
507
|
-
|
508
|
-
|
509
|
-
|
510
|
-
|
511
|
-
|
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
|
-
|
530
|
-
|
531
|
-
|
532
|
-
|
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
|
-
|
532
|
+
tail_tags = tagged[:synonyms].concat(tagged[:transformed])
|
533
|
+
tail_tags.sort!
|
534
|
+
tail_tags.uniq!
|
539
535
|
|
540
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
569
|
-
|
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
|
-
|
572
|
-
|
566
|
+
section = guess_section(section)
|
567
|
+
destination = guess_section(destination)
|
573
568
|
|
574
|
-
|
575
|
-
|
569
|
+
section_items = @content.in_section(section)
|
570
|
+
max = section_items.count - count.to_i
|
576
571
|
|
577
|
-
|
572
|
+
opt[:after] = opt[:from][0] if opt[:from]
|
573
|
+
opt[:before] = opt[:from][1] if opt[:from]
|
578
574
|
|
579
|
-
|
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
|
-
|
584
|
-
|
585
|
-
|
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
|
-
|
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
|
-
|
590
|
-
break if counter >= max
|
585
|
+
counter = 0
|
591
586
|
|
592
|
-
|
587
|
+
@content.map do |item|
|
588
|
+
break if counter >= max
|
593
589
|
|
594
|
-
|
590
|
+
next if item.section.downcase == destination.downcase
|
595
591
|
|
596
|
-
|
592
|
+
next if item.section.downcase != section.downcase && section != /^all$/i
|
597
593
|
|
598
|
-
|
594
|
+
next if (opt[:before] && item.date > opt[:before]) || (opt[:after] && item.date < opt[:after])
|
599
595
|
|
600
|
-
|
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
|
-
|
608
|
-
|
609
|
-
|
610
|
-
|
611
|
-
|
612
|
-
|
613
|
-
|
614
|
-
|
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
|