doing 2.1.39 → 2.1.40
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/CHANGELOG.md +23 -0
- data/Gemfile.lock +1 -1
- data/README.md +1 -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/on.rb +3 -16
- 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 +6 -17
- 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 +15 -8
- 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 +90 -1615
- 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 +293 -773
- data/docs/doc/top-level-namespace.html +3 -3
- data/docs/index.md +1 -1
- data/doing.rdoc +49 -7
- data/lib/completion/_doing.zsh +5 -5
- data/lib/completion/doing.bash +8 -8
- data/lib/completion/doing.fish +7 -2
- data/lib/doing/add_options.rb +31 -1
- data/lib/doing/chronify/array.rb +64 -22
- 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 -667
- data/lib/doing/items.rb +38 -13
- 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/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 -2317
- 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
|