doing 2.1.37 → 2.1.40
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +61 -0
- data/Gemfile.lock +1 -1
- data/README.md +1 -1
- data/Rakefile +7 -1
- data/bin/commands/config.rb +43 -34
- data/bin/commands/done.rb +1 -18
- data/bin/commands/finish.rb +30 -25
- data/bin/commands/grep.rb +3 -14
- data/bin/commands/last.rb +2 -8
- data/bin/commands/meanwhile.rb +13 -6
- data/bin/commands/now.rb +2 -4
- data/bin/commands/on.rb +4 -15
- data/bin/commands/recent.rb +2 -8
- data/bin/commands/reset.rb +24 -1
- data/bin/commands/select.rb +1 -1
- data/bin/commands/show.rb +8 -16
- data/bin/commands/since.rb +1 -12
- data/bin/commands/today.rb +2 -13
- data/bin/commands/view.rb +1 -1
- data/bin/commands/yesterday.rb +2 -13
- data/bin/doing +41 -36
- data/docs/doc/Array.html +1 -1
- 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/Color.html +166 -20
- data/docs/doc/Doing/Completion.html +1 -1
- data/docs/doc/Doing/Configuration.html +1 -1
- data/docs/doc/Doing/Errors/DoingNoTraceError.html +7 -3
- data/docs/doc/Doing/Errors/DoingRuntimeError.html +7 -3
- data/docs/doc/Doing/Errors/DoingStandardError.html +1 -1
- data/docs/doc/Doing/Errors/EmptyInput.html +10 -2
- data/docs/doc/Doing/Errors/HistoryLimitError.html +194 -0
- data/docs/doc/Doing/Errors/InvalidPlugin.html +194 -0
- data/docs/doc/Doing/Errors/MissingBackupFile.html +194 -0
- data/docs/doc/Doing/Errors/NoResults.html +10 -2
- data/docs/doc/Doing/Errors/PluginException.html +1 -1
- data/docs/doc/Doing/Errors/UserCancelled.html +10 -2
- data/docs/doc/Doing/Errors/WrongCommand.html +10 -2
- data/docs/doc/Doing/Errors.html +9 -9
- data/docs/doc/Doing/Hooks.html +1 -1
- data/docs/doc/Doing/Item.html +114 -1576
- data/docs/doc/Doing/Items.html +121 -5
- data/docs/doc/Doing/Logger.html +1 -1
- data/docs/doc/Doing/Note.html +1 -1
- data/docs/doc/Doing/Pager.html +1 -1
- data/docs/doc/Doing/Plugins.html +1 -1
- data/docs/doc/Doing/Prompt.html +2 -2
- data/docs/doc/Doing/Section.html +1 -1
- data/docs/doc/Doing/TemplateString.html +2 -2
- data/docs/doc/Doing/Types.html +1 -1
- data/docs/doc/Doing/Util/Backup.html +5 -5
- data/docs/doc/Doing/Util.html +1 -1
- data/docs/doc/Doing/WWID.html +197 -4033
- data/docs/doc/Doing.html +2 -2
- 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/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 +1 -1
- data/docs/doc/Symbol.html +1 -1
- data/docs/doc/Time.html +1 -1
- data/docs/doc/TrueClass.html +1 -1
- data/docs/doc/_index.html +26 -5
- data/docs/doc/class_list.html +1 -1
- data/docs/doc/file.README.html +2 -2
- data/docs/doc/index.html +2 -2
- data/docs/doc/method_list.html +237 -709
- data/docs/doc/top-level-namespace.html +3 -3
- data/docs/index.md +1 -1
- data/doing.rdoc +54 -7
- data/lib/completion/_doing.zsh +6 -6
- data/lib/completion/doing.bash +10 -10
- data/lib/completion/doing.fish +8 -2
- data/lib/doing/add_options.rb +31 -1
- data/lib/doing/chronify/array.rb +68 -18
- data/lib/doing/chronify/string.rb +3 -1
- data/lib/doing/colors.rb +77 -30
- data/lib/doing/completion.rb +4 -5
- data/lib/doing/errors.rb +51 -35
- data/lib/doing/hooks.rb +3 -3
- data/lib/doing/item/dates.rb +112 -0
- data/lib/doing/item/query.rb +433 -0
- data/lib/doing/item/state.rb +59 -0
- data/lib/doing/item/tags.rb +87 -0
- data/lib/doing/item.rb +6 -537
- data/lib/doing/items.rb +39 -14
- data/lib/doing/plugin_manager.rb +3 -3
- data/lib/doing/plugins/export/template_export.rb +4 -4
- data/lib/doing/plugins/import/cal_to_json.scpt +0 -0
- data/lib/doing/prompt.rb +6 -8
- data/lib/doing/string/tags.rb +8 -2
- data/lib/doing/util_backup.rb +6 -8
- data/lib/doing/version.rb +1 -1
- data/lib/doing/wwid/display.rb +399 -0
- data/lib/doing/wwid/editor.rb +214 -0
- data/lib/doing/wwid/filetools.rb +186 -0
- data/lib/doing/wwid/filter.rb +218 -0
- data/lib/doing/wwid/guess.rb +87 -0
- data/lib/doing/wwid/interactive.rb +385 -0
- data/lib/doing/wwid/modify.rb +618 -0
- data/lib/doing/wwid/tags.rb +54 -0
- data/lib/doing/wwid/timers.rb +345 -0
- data/lib/doing/wwid/wwidutil.rb +104 -0
- data/lib/doing/wwid.rb +31 -2308
- metadata +19 -2
@@ -0,0 +1,433 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Doing
|
4
|
+
# Tag and search filtering for a Doing entry
|
5
|
+
class Item
|
6
|
+
##
|
7
|
+
## Test if item contains tag(s)
|
8
|
+
##
|
9
|
+
## @param tags (Array or String) The tags to test. Can be an array or a comma-separated string.
|
10
|
+
## @param bool (Symbol) The boolean to use for multiple tags (:and, :or, :not)
|
11
|
+
## @param negate [Boolean] negate the result?
|
12
|
+
##
|
13
|
+
## @return [Boolean] true if tag/bool combination passes
|
14
|
+
##
|
15
|
+
def tags?(tags, bool = :and, negate: false)
|
16
|
+
if bool == :pattern
|
17
|
+
tags = tags.to_tags.tags_to_array.join(' ')
|
18
|
+
matches = tag_pattern?(tags)
|
19
|
+
|
20
|
+
return negate ? !matches : matches
|
21
|
+
end
|
22
|
+
|
23
|
+
tags = split_tags(tags)
|
24
|
+
bool = bool.normalize_bool
|
25
|
+
|
26
|
+
matches = case bool
|
27
|
+
when :and
|
28
|
+
all_tags?(tags)
|
29
|
+
when :not
|
30
|
+
no_tags?(tags)
|
31
|
+
else
|
32
|
+
any_tags?(tags)
|
33
|
+
end
|
34
|
+
negate ? !matches : matches
|
35
|
+
end
|
36
|
+
|
37
|
+
##
|
38
|
+
## Test if item matches tag values
|
39
|
+
##
|
40
|
+
## @param queries (Array) The tag value queries to test
|
41
|
+
## @param bool (Symbol) The boolean to use for multiple tags (:and, :or, :not)
|
42
|
+
## @param negate [Boolean] negate the result?
|
43
|
+
##
|
44
|
+
## @return [Boolean] true if tag/bool combination passes
|
45
|
+
##
|
46
|
+
def tag_values?(queries, bool = :and, negate: false)
|
47
|
+
bool = bool.normalize_bool
|
48
|
+
|
49
|
+
matches = case bool
|
50
|
+
when :and
|
51
|
+
all_values?(queries)
|
52
|
+
when :not
|
53
|
+
no_values?(queries)
|
54
|
+
else
|
55
|
+
any_values?(queries)
|
56
|
+
end
|
57
|
+
negate ? !matches : matches
|
58
|
+
end
|
59
|
+
|
60
|
+
##
|
61
|
+
## Determine if case should be ignored for searches
|
62
|
+
##
|
63
|
+
## @param search [String] The search string
|
64
|
+
## @param case_type [Symbol] The case type
|
65
|
+
##
|
66
|
+
## @return [Boolean] case should be ignored
|
67
|
+
##
|
68
|
+
def ignore_case(search, case_type)
|
69
|
+
(case_type == :smart && search !~ /[A-Z]/) || case_type == :ignore
|
70
|
+
end
|
71
|
+
|
72
|
+
def highlight_search(search, distance: nil, negate: false, case_type: nil)
|
73
|
+
prefs = Doing.setting('search', {})
|
74
|
+
matching = prefs.fetch('matching', 'pattern').normalize_matching
|
75
|
+
distance ||= prefs.fetch('distance', 3).to_i
|
76
|
+
case_type ||= prefs.fetch('case', 'smart').normalize_case
|
77
|
+
new_note = Note.new
|
78
|
+
|
79
|
+
if search.rx? || matching == :fuzzy
|
80
|
+
rx = search.to_rx(distance: distance, case_type: case_type)
|
81
|
+
new_title = @title.gsub(rx) { |m| yellow(m) }
|
82
|
+
new_note.add(@note.to_s.gsub(rx) { |m| yellow(m) })
|
83
|
+
else
|
84
|
+
query = search.strip.to_phrase_query
|
85
|
+
|
86
|
+
if query[:must].nil? && query[:must_not].nil?
|
87
|
+
query[:must] = query[:should]
|
88
|
+
query[:should] = []
|
89
|
+
end
|
90
|
+
query[:must].concat(query[:should]).each do |s|
|
91
|
+
rx = Regexp.new(s.wildcard_to_rx, ignore_case(s, case_type))
|
92
|
+
new_title = @title.gsub(rx) { |m| yellow(m) }
|
93
|
+
new_note.add(@note.to_s.gsub(rx) { |m| yellow(m) })
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
Item.new(@date, new_title, @section, new_note)
|
98
|
+
end
|
99
|
+
|
100
|
+
##
|
101
|
+
## Test if item matches search string
|
102
|
+
##
|
103
|
+
## @param search [String] The search string
|
104
|
+
## @param negate [Boolean] negate results
|
105
|
+
## @param case_type (Symbol) The case-sensitivity
|
106
|
+
## type (:sensitive,
|
107
|
+
## :ignore, :smart)
|
108
|
+
##
|
109
|
+
## @return [Boolean] matches search criteria
|
110
|
+
##
|
111
|
+
def search(search, distance: nil, negate: false, case_type: nil)
|
112
|
+
prefs = Doing.setting('search', {})
|
113
|
+
matching = prefs.fetch('matching', 'pattern').normalize_matching
|
114
|
+
distance ||= prefs.fetch('distance', 3).to_i
|
115
|
+
case_type ||= prefs.fetch('case', 'smart').normalize_case
|
116
|
+
|
117
|
+
if search.rx? || matching == :fuzzy
|
118
|
+
matches = @title + @note.to_s =~ search.to_rx(distance: distance, case_type: case_type)
|
119
|
+
else
|
120
|
+
query = search.strip.to_phrase_query
|
121
|
+
|
122
|
+
if query[:must].nil? && query[:must_not].nil?
|
123
|
+
query[:must] = query[:should]
|
124
|
+
query[:should] = []
|
125
|
+
end
|
126
|
+
matches = no_searches?(query[:must_not], case_type: case_type)
|
127
|
+
matches &&= all_searches?(query[:must], case_type: case_type)
|
128
|
+
matches &&= any_searches?(query[:should], case_type: case_type)
|
129
|
+
end
|
130
|
+
# if search =~ /(?<=\A| )[+-]\S/
|
131
|
+
# else
|
132
|
+
# text = @title + @note.to_s
|
133
|
+
# matches = text =~ search.to_rx(distance: distance, case_type: case_type)
|
134
|
+
# end
|
135
|
+
|
136
|
+
# if search.rx? || !fuzzy
|
137
|
+
# matches = text =~ search.to_rx(distance: distance, case_type: case_type)
|
138
|
+
# else
|
139
|
+
# distance = 0.25 if distance > 1
|
140
|
+
# score = if (case_type == :smart && search !~ /[A-Z]/) || case_type == :ignore
|
141
|
+
# text.downcase.pair_distance_similar(search.downcase)
|
142
|
+
# else
|
143
|
+
# score = text.pair_distance_similar(search)
|
144
|
+
# end
|
145
|
+
|
146
|
+
# if score >= distance
|
147
|
+
# matches = true
|
148
|
+
# Doing.logger.debug('Fuzzy Match:', %(#{@title}, "#{search}" #{score}))
|
149
|
+
# end
|
150
|
+
# end
|
151
|
+
|
152
|
+
negate ? !matches : matches
|
153
|
+
end
|
154
|
+
|
155
|
+
private
|
156
|
+
|
157
|
+
def all_searches?(searches, case_type: :smart)
|
158
|
+
return true unless searches.good?
|
159
|
+
|
160
|
+
text = @title + @note.to_s
|
161
|
+
searches.each do |s|
|
162
|
+
rx = Regexp.new(s.wildcard_to_rx, ignore_case(s, case_type))
|
163
|
+
return false unless text =~ rx
|
164
|
+
end
|
165
|
+
true
|
166
|
+
end
|
167
|
+
|
168
|
+
def no_searches?(searches, case_type: :smart)
|
169
|
+
return true unless searches.good?
|
170
|
+
|
171
|
+
text = @title + @note.to_s
|
172
|
+
searches.each do |s|
|
173
|
+
rx = Regexp.new(s.wildcard_to_rx, ignore_case(s, case_type))
|
174
|
+
return false if text =~ rx
|
175
|
+
end
|
176
|
+
true
|
177
|
+
end
|
178
|
+
|
179
|
+
def any_searches?(searches, case_type: :smart)
|
180
|
+
return true unless searches.good?
|
181
|
+
|
182
|
+
text = @title + @note.to_s
|
183
|
+
searches.each do |s|
|
184
|
+
rx = Regexp.new(s.wildcard_to_rx, ignore_case(s, case_type))
|
185
|
+
return true if text =~ rx
|
186
|
+
end
|
187
|
+
false
|
188
|
+
end
|
189
|
+
|
190
|
+
def all_tags?(tags)
|
191
|
+
return true unless tags.good?
|
192
|
+
|
193
|
+
tags.each do |tag|
|
194
|
+
if tag =~ /done/ && !should_finish?
|
195
|
+
next
|
196
|
+
else
|
197
|
+
return false unless @title =~ /@#{tag.wildcard_to_rx}(?= |\(|\Z)/i
|
198
|
+
end
|
199
|
+
end
|
200
|
+
true
|
201
|
+
end
|
202
|
+
|
203
|
+
def no_tags?(tags)
|
204
|
+
return true unless tags.good?
|
205
|
+
|
206
|
+
tags.each do |tag|
|
207
|
+
if tag =~ /done/ && !should_finish?
|
208
|
+
return false
|
209
|
+
else
|
210
|
+
return false if @title =~ /@#{tag.wildcard_to_rx}(?= |\(|\Z)/i
|
211
|
+
end
|
212
|
+
end
|
213
|
+
true
|
214
|
+
end
|
215
|
+
|
216
|
+
def any_tags?(tags)
|
217
|
+
return true unless tags.good?
|
218
|
+
|
219
|
+
tags.each do |tag|
|
220
|
+
if tag =~ /done/ && !should_finish?
|
221
|
+
return true
|
222
|
+
else
|
223
|
+
return true if @title =~ /@#{tag.wildcard_to_rx}(?= |\(|\Z)/i
|
224
|
+
end
|
225
|
+
end
|
226
|
+
false
|
227
|
+
end
|
228
|
+
|
229
|
+
def tag_pattern?(tags)
|
230
|
+
query = tags.to_query
|
231
|
+
|
232
|
+
no_tags?(query[:must_not]) && all_tags?(query[:must]) && any_tags?(query[:should])
|
233
|
+
end
|
234
|
+
|
235
|
+
def tag_value(tag)
|
236
|
+
res = @title.match(/@#{tag.sub(/^@/, '').wildcard_to_rx}\((.*?)\)/)
|
237
|
+
res ? res[1] : nil
|
238
|
+
end
|
239
|
+
|
240
|
+
def number_or_date(value)
|
241
|
+
return nil unless value
|
242
|
+
|
243
|
+
if value.strip =~ /^[0-9.]+%?$/
|
244
|
+
value.strip.to_f
|
245
|
+
else
|
246
|
+
value.strip.chronify(guess: :end)
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
def split_value_query(query)
|
251
|
+
val_rx = /^(!)?@?(\S+) +(!?[<>=][=*]?|[$*^]=) +(.*?)$/
|
252
|
+
query.match(val_rx)
|
253
|
+
end
|
254
|
+
|
255
|
+
def any_values?(queries)
|
256
|
+
return true unless queries.good?
|
257
|
+
|
258
|
+
queries.each do |q|
|
259
|
+
parts = split_value_query(q)
|
260
|
+
return true if tag_value_matches?(parts[2], parts[3], parts[4], parts[1])
|
261
|
+
end
|
262
|
+
false
|
263
|
+
end
|
264
|
+
|
265
|
+
def all_values?(queries)
|
266
|
+
return true unless queries.good?
|
267
|
+
|
268
|
+
queries.each do |q|
|
269
|
+
parts = split_value_query(q)
|
270
|
+
|
271
|
+
return false unless tag_value_matches?(parts[2], parts[3], parts[4], parts[1])
|
272
|
+
end
|
273
|
+
true
|
274
|
+
end
|
275
|
+
|
276
|
+
def no_values?(queries)
|
277
|
+
return true unless queries.good?
|
278
|
+
|
279
|
+
queries.each do |q|
|
280
|
+
parts = split_value_query(q)
|
281
|
+
return false if tag_value_matches?(parts[2], parts[3], parts[4], parts[1])
|
282
|
+
end
|
283
|
+
true
|
284
|
+
end
|
285
|
+
|
286
|
+
def duration_matches?(value, comp)
|
287
|
+
return false if interval.nil?
|
288
|
+
|
289
|
+
val = value.chronify_qty
|
290
|
+
case comp
|
291
|
+
when /^<$/
|
292
|
+
interval < val
|
293
|
+
when /^<=$/
|
294
|
+
interval <= val
|
295
|
+
when /^>$/
|
296
|
+
interval > val
|
297
|
+
when /^>=$/
|
298
|
+
interval >= val
|
299
|
+
when /^!=/
|
300
|
+
interval != val
|
301
|
+
when /^=/
|
302
|
+
interval == val
|
303
|
+
end
|
304
|
+
end
|
305
|
+
|
306
|
+
def date_matches?(value, comp)
|
307
|
+
time_rx = /^(\d{1,2}+(:\d{1,2}+)?( *(am|pm))?|midnight|noon)$/i
|
308
|
+
value = "#{@date.strftime('%Y-%m-%d')} #{value}" if value =~ time_rx
|
309
|
+
|
310
|
+
val = value.chronify(guess: :begin)
|
311
|
+
raise InvalidTimeExpression, "Unrecognized date/time expression (#{value})" if val.nil?
|
312
|
+
|
313
|
+
case comp
|
314
|
+
when /^<$/
|
315
|
+
@date < val
|
316
|
+
when /^<=$/
|
317
|
+
@date <= val
|
318
|
+
when /^>$/
|
319
|
+
@date > val
|
320
|
+
when /^>=$/
|
321
|
+
@date >= val
|
322
|
+
when /^!=/
|
323
|
+
@date != val
|
324
|
+
when /^=/
|
325
|
+
@date == val
|
326
|
+
end
|
327
|
+
end
|
328
|
+
|
329
|
+
def value_string_matches?(tag_val, comp, value)
|
330
|
+
case comp
|
331
|
+
when /\^=/
|
332
|
+
tag_val =~ /^#{value.wildcard_to_rx}/i
|
333
|
+
when /\$=/
|
334
|
+
tag_val =~ /#{value.wildcard_to_rx}$/i
|
335
|
+
when %r{==}
|
336
|
+
tag_val =~ /^#{value.wildcard_to_rx}$/i
|
337
|
+
else
|
338
|
+
tag_val =~ /#{value.wildcard_to_rx}/i
|
339
|
+
end
|
340
|
+
end
|
341
|
+
|
342
|
+
def value_number_matches?(tag_val, comp, value)
|
343
|
+
case comp
|
344
|
+
when /^<$/
|
345
|
+
tag_val < value
|
346
|
+
when /^<=$/
|
347
|
+
tag_val <= value
|
348
|
+
when /^>$/
|
349
|
+
tag_val > value
|
350
|
+
when /^>=$/
|
351
|
+
tag_val >= value
|
352
|
+
when /^!=/
|
353
|
+
tag_val != value
|
354
|
+
when /^=/
|
355
|
+
tag_val == value
|
356
|
+
end
|
357
|
+
end
|
358
|
+
|
359
|
+
##
|
360
|
+
## Test if a tag's value matches a given value. Value
|
361
|
+
## can be a date string, a text string, or a
|
362
|
+
## number/percentage. Type of comparison is determined
|
363
|
+
## by the comparitor and the objects being compared.
|
364
|
+
##
|
365
|
+
## @param tag [String] The tag name from which
|
366
|
+
## to get the value
|
367
|
+
## @param comp [String] The comparator (e.g. >=
|
368
|
+
## or *=)
|
369
|
+
## @param value [String] The value to test
|
370
|
+
## against
|
371
|
+
## @param negate [Boolean] Negate the response
|
372
|
+
##
|
373
|
+
## @return True if tag value matches, False otherwise.
|
374
|
+
##
|
375
|
+
def tag_value_matches?(tag, comp, value, negate)
|
376
|
+
# If tag matches existing tag
|
377
|
+
if tags?(tag, :and)
|
378
|
+
tag_val = tag_value(tag)
|
379
|
+
|
380
|
+
# If the tag value is not a date and contains alpha
|
381
|
+
# characters and comparison is ==, or comparison is
|
382
|
+
# a string comparitor (*= ^= $=)
|
383
|
+
if (value.chronify.nil? && value =~ /[a-z]/i && comp =~ /^!?==?$/) || comp =~ /[$*^]=/
|
384
|
+
is_match = value_string_matches?(tag_val, comp, value)
|
385
|
+
|
386
|
+
comp =~ /!/ || negate ? !is_match : is_match
|
387
|
+
else
|
388
|
+
# Convert values to either a number or a date
|
389
|
+
tag_val = number_or_date(tag_val)
|
390
|
+
val = number_or_date(value)
|
391
|
+
|
392
|
+
# Fail if either value is nil
|
393
|
+
return false if val.nil? || tag_val.nil?
|
394
|
+
|
395
|
+
# Fail unless both values are of the same class (float or date)
|
396
|
+
return false unless val.class == tag_val.class
|
397
|
+
|
398
|
+
is_match = value_number_matches?(tag_val, comp, val)
|
399
|
+
|
400
|
+
negate.nil? ? is_match : !is_match
|
401
|
+
end
|
402
|
+
# If tag name matches a trigger for elapsed time test
|
403
|
+
elsif tag =~ /^(elapsed|dur(ation)?|int(erval)?)$/i
|
404
|
+
is_match = duration_matches?(value, comp)
|
405
|
+
|
406
|
+
comp =~ /!/ || negate ? !is_match : is_match
|
407
|
+
# Else if tag name matches a trigger for start date
|
408
|
+
elsif tag =~ /^(d(ate)?|t(ime)?)$/i
|
409
|
+
is_match = date_matches?(value, comp)
|
410
|
+
|
411
|
+
comp =~ /!/ || negate ? !is_match : is_match
|
412
|
+
# Else if tag name matches a trigger for all text
|
413
|
+
elsif tag =~ /^text$/i
|
414
|
+
is_match = value_string_matches?([@title, @note.to_s(prefix: '')].join(' '), comp, value)
|
415
|
+
|
416
|
+
comp =~ /!/ || negate ? !is_match : is_match
|
417
|
+
# Else if tag name matches a trigger for title
|
418
|
+
elsif tag =~ /^title$/i
|
419
|
+
is_match = value_string_matches?(@title, comp, value)
|
420
|
+
|
421
|
+
comp =~ /!/ || negate ? !is_match : is_match
|
422
|
+
# Else if tag name matches a trigger for note
|
423
|
+
elsif tag =~ /^note$/i
|
424
|
+
is_match = value_string_matches?(@note.to_s(prefix: ''), comp, value)
|
425
|
+
|
426
|
+
comp =~ /!/ || negate ? !is_match : is_match
|
427
|
+
# Else if item contains tag being tested
|
428
|
+
else
|
429
|
+
false
|
430
|
+
end
|
431
|
+
end
|
432
|
+
end
|
433
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module Doing
|
2
|
+
# State queries for a Doing entry
|
3
|
+
class Item
|
4
|
+
##
|
5
|
+
## Test if item has a @done tag
|
6
|
+
##
|
7
|
+
## @return [Boolean] true item has @done tag
|
8
|
+
##
|
9
|
+
def finished?
|
10
|
+
tags?('done')
|
11
|
+
end
|
12
|
+
|
13
|
+
##
|
14
|
+
## Test if item does not contain @done tag
|
15
|
+
##
|
16
|
+
## @return [Boolean] true if item is missing @done tag
|
17
|
+
##
|
18
|
+
def unfinished?
|
19
|
+
tags?('done', negate: true)
|
20
|
+
end
|
21
|
+
|
22
|
+
##
|
23
|
+
## Test if item is included in never_finish config and
|
24
|
+
## thus should not receive a @done tag
|
25
|
+
##
|
26
|
+
## @return [Boolean] item should receive @done tag
|
27
|
+
##
|
28
|
+
def should_finish?
|
29
|
+
should?('never_finish')
|
30
|
+
end
|
31
|
+
|
32
|
+
##
|
33
|
+
## Test if item is included in never_time config and
|
34
|
+
## thus should not receive a date on the @done tag
|
35
|
+
##
|
36
|
+
## @return [Boolean] item should receive @done date
|
37
|
+
##
|
38
|
+
def should_time?
|
39
|
+
should?('never_time')
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def should?(key)
|
45
|
+
config = Doing.settings
|
46
|
+
return true unless config[key].is_a?(Array)
|
47
|
+
|
48
|
+
config[key].each do |tag|
|
49
|
+
if tag =~ /^@/
|
50
|
+
return false if tags?(tag.sub(/^@/, '').downcase)
|
51
|
+
elsif section.downcase == tag.downcase
|
52
|
+
return false
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
true
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Doing
|
4
|
+
# A Doing entry
|
5
|
+
class Item
|
6
|
+
##
|
7
|
+
## Add (or remove) tags from the title of the item
|
8
|
+
##
|
9
|
+
## @param tags [Array] The tags to apply
|
10
|
+
## @param options Additional options
|
11
|
+
##
|
12
|
+
## @option options :date [Boolean] Include timestamp?
|
13
|
+
## @option options :single [Boolean] Log as a single change?
|
14
|
+
## @option options :value [String] A value to include as @tag(value)
|
15
|
+
## @option options :remove [Boolean] if true remove instead of adding
|
16
|
+
## @option options :rename_to [String] if not nil, rename target tag to this tag name
|
17
|
+
## @option options :regex [Boolean] treat target tag string as regex pattern
|
18
|
+
## @option options :force [Boolean] with rename_to, add tag if it doesn't exist
|
19
|
+
##
|
20
|
+
def tag(tags, **options)
|
21
|
+
added = []
|
22
|
+
removed = []
|
23
|
+
|
24
|
+
date = options.fetch(:date, false)
|
25
|
+
options[:value] ||= date ? Time.now.strftime('%F %R') : nil
|
26
|
+
options.delete(:date)
|
27
|
+
|
28
|
+
single = options.fetch(:single, false)
|
29
|
+
options.delete(:single)
|
30
|
+
|
31
|
+
tags = tags.to_tags if tags.is_a? ::String
|
32
|
+
|
33
|
+
remove = options.fetch(:remove, false)
|
34
|
+
tags.each do |tag|
|
35
|
+
if tag =~ /^(\S+)\((.*?)\)$/
|
36
|
+
m = Regexp.last_match
|
37
|
+
tag = m[1]
|
38
|
+
options[:value] ||= m[2]
|
39
|
+
end
|
40
|
+
|
41
|
+
bool = remove ? :and : :not
|
42
|
+
if tags?(tag, bool) || options[:value]
|
43
|
+
@title = @title.tag(tag, **options).strip
|
44
|
+
remove ? removed.push(tag) : added.push(tag)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
Doing.logger.log_change(tags_added: added, tags_removed: removed, count: 1, item: self, single: single)
|
49
|
+
|
50
|
+
self
|
51
|
+
end
|
52
|
+
|
53
|
+
##
|
54
|
+
## Get a list of tags on the item
|
55
|
+
##
|
56
|
+
## @return [Array] array of tags (no values)
|
57
|
+
##
|
58
|
+
def tags
|
59
|
+
@title.scan(/(?<= |\A)@([^\s(]+)/).map { |tag| tag[0] }.sort.uniq
|
60
|
+
end
|
61
|
+
|
62
|
+
##
|
63
|
+
## Return all tags including parenthetical values
|
64
|
+
##
|
65
|
+
## @return [Array<Array>] Array of array pairs,
|
66
|
+
## [[tag1, value], [tag2, value]]
|
67
|
+
##
|
68
|
+
def tags_with_values
|
69
|
+
@title.scan(/(?<= |\A)@([^\s(]+)(?:\((.*?)\))?/).map { |tag| [tag[0], tag[1]] }.sort.uniq
|
70
|
+
end
|
71
|
+
|
72
|
+
##
|
73
|
+
## convert tags on item to an array with @ symbols removed
|
74
|
+
##
|
75
|
+
## @return [Array] array of tags
|
76
|
+
##
|
77
|
+
def tag_array
|
78
|
+
tags.tags_to_array
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
def split_tags(tags)
|
84
|
+
tags.to_tags.tags_to_array
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|