doing 2.1.40 → 2.1.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.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
|