doing 2.1.37 → 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.
Files changed (121) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +61 -0
  3. data/Gemfile.lock +1 -1
  4. data/README.md +1 -1
  5. data/Rakefile +7 -1
  6. data/bin/commands/config.rb +43 -34
  7. data/bin/commands/done.rb +1 -18
  8. data/bin/commands/finish.rb +30 -25
  9. data/bin/commands/grep.rb +3 -14
  10. data/bin/commands/last.rb +2 -8
  11. data/bin/commands/meanwhile.rb +13 -6
  12. data/bin/commands/now.rb +2 -4
  13. data/bin/commands/on.rb +4 -15
  14. data/bin/commands/recent.rb +2 -8
  15. data/bin/commands/reset.rb +24 -1
  16. data/bin/commands/select.rb +1 -1
  17. data/bin/commands/show.rb +8 -16
  18. data/bin/commands/since.rb +1 -12
  19. data/bin/commands/today.rb +2 -13
  20. data/bin/commands/view.rb +1 -1
  21. data/bin/commands/yesterday.rb +2 -13
  22. data/bin/doing +41 -36
  23. data/docs/doc/Array.html +1 -1
  24. data/docs/doc/BooleanTermParser/Clause.html +1 -1
  25. data/docs/doc/BooleanTermParser/Operator.html +1 -1
  26. data/docs/doc/BooleanTermParser/Query.html +1 -1
  27. data/docs/doc/BooleanTermParser/QueryParser.html +1 -1
  28. data/docs/doc/BooleanTermParser/QueryTransformer.html +1 -1
  29. data/docs/doc/BooleanTermParser.html +1 -1
  30. data/docs/doc/Doing/Color.html +166 -20
  31. data/docs/doc/Doing/Completion.html +1 -1
  32. data/docs/doc/Doing/Configuration.html +1 -1
  33. data/docs/doc/Doing/Errors/DoingNoTraceError.html +7 -3
  34. data/docs/doc/Doing/Errors/DoingRuntimeError.html +7 -3
  35. data/docs/doc/Doing/Errors/DoingStandardError.html +1 -1
  36. data/docs/doc/Doing/Errors/EmptyInput.html +10 -2
  37. data/docs/doc/Doing/Errors/HistoryLimitError.html +194 -0
  38. data/docs/doc/Doing/Errors/InvalidPlugin.html +194 -0
  39. data/docs/doc/Doing/Errors/MissingBackupFile.html +194 -0
  40. data/docs/doc/Doing/Errors/NoResults.html +10 -2
  41. data/docs/doc/Doing/Errors/PluginException.html +1 -1
  42. data/docs/doc/Doing/Errors/UserCancelled.html +10 -2
  43. data/docs/doc/Doing/Errors/WrongCommand.html +10 -2
  44. data/docs/doc/Doing/Errors.html +9 -9
  45. data/docs/doc/Doing/Hooks.html +1 -1
  46. data/docs/doc/Doing/Item.html +114 -1576
  47. data/docs/doc/Doing/Items.html +121 -5
  48. data/docs/doc/Doing/Logger.html +1 -1
  49. data/docs/doc/Doing/Note.html +1 -1
  50. data/docs/doc/Doing/Pager.html +1 -1
  51. data/docs/doc/Doing/Plugins.html +1 -1
  52. data/docs/doc/Doing/Prompt.html +2 -2
  53. data/docs/doc/Doing/Section.html +1 -1
  54. data/docs/doc/Doing/TemplateString.html +2 -2
  55. data/docs/doc/Doing/Types.html +1 -1
  56. data/docs/doc/Doing/Util/Backup.html +5 -5
  57. data/docs/doc/Doing/Util.html +1 -1
  58. data/docs/doc/Doing/WWID.html +197 -4033
  59. data/docs/doc/Doing.html +2 -2
  60. data/docs/doc/FalseClass.html +1 -1
  61. data/docs/doc/GLI/Commands/Help.html +1 -1
  62. data/docs/doc/GLI/Commands/MarkdownDocumentListener.html +1 -1
  63. data/docs/doc/GLI/Commands.html +1 -1
  64. data/docs/doc/GLI.html +1 -1
  65. data/docs/doc/Hash.html +1 -1
  66. data/docs/doc/Object.html +1 -1
  67. data/docs/doc/PhraseParser/Operator.html +1 -1
  68. data/docs/doc/PhraseParser/PhraseClause.html +1 -1
  69. data/docs/doc/PhraseParser/Query.html +1 -1
  70. data/docs/doc/PhraseParser/QueryParser.html +1 -1
  71. data/docs/doc/PhraseParser/QueryTransformer.html +1 -1
  72. data/docs/doc/PhraseParser/TermClause.html +1 -1
  73. data/docs/doc/PhraseParser.html +1 -1
  74. data/docs/doc/Status.html +1 -1
  75. data/docs/doc/String.html +1 -1
  76. data/docs/doc/Symbol.html +1 -1
  77. data/docs/doc/Time.html +1 -1
  78. data/docs/doc/TrueClass.html +1 -1
  79. data/docs/doc/_index.html +26 -5
  80. data/docs/doc/class_list.html +1 -1
  81. data/docs/doc/file.README.html +2 -2
  82. data/docs/doc/index.html +2 -2
  83. data/docs/doc/method_list.html +237 -709
  84. data/docs/doc/top-level-namespace.html +3 -3
  85. data/docs/index.md +1 -1
  86. data/doing.rdoc +54 -7
  87. data/lib/completion/_doing.zsh +6 -6
  88. data/lib/completion/doing.bash +10 -10
  89. data/lib/completion/doing.fish +8 -2
  90. data/lib/doing/add_options.rb +31 -1
  91. data/lib/doing/chronify/array.rb +68 -18
  92. data/lib/doing/chronify/string.rb +3 -1
  93. data/lib/doing/colors.rb +77 -30
  94. data/lib/doing/completion.rb +4 -5
  95. data/lib/doing/errors.rb +51 -35
  96. data/lib/doing/hooks.rb +3 -3
  97. data/lib/doing/item/dates.rb +112 -0
  98. data/lib/doing/item/query.rb +433 -0
  99. data/lib/doing/item/state.rb +59 -0
  100. data/lib/doing/item/tags.rb +87 -0
  101. data/lib/doing/item.rb +6 -537
  102. data/lib/doing/items.rb +39 -14
  103. data/lib/doing/plugin_manager.rb +3 -3
  104. data/lib/doing/plugins/export/template_export.rb +4 -4
  105. data/lib/doing/plugins/import/cal_to_json.scpt +0 -0
  106. data/lib/doing/prompt.rb +6 -8
  107. data/lib/doing/string/tags.rb +8 -2
  108. data/lib/doing/util_backup.rb +6 -8
  109. data/lib/doing/version.rb +1 -1
  110. data/lib/doing/wwid/display.rb +399 -0
  111. data/lib/doing/wwid/editor.rb +214 -0
  112. data/lib/doing/wwid/filetools.rb +186 -0
  113. data/lib/doing/wwid/filter.rb +218 -0
  114. data/lib/doing/wwid/guess.rb +87 -0
  115. data/lib/doing/wwid/interactive.rb +385 -0
  116. data/lib/doing/wwid/modify.rb +618 -0
  117. data/lib/doing/wwid/tags.rb +54 -0
  118. data/lib/doing/wwid/timers.rb +345 -0
  119. data/lib/doing/wwid/wwidutil.rb +104 -0
  120. data/lib/doing/wwid.rb +31 -2308
  121. metadata +19 -2
data/lib/doing/item.rb CHANGED
@@ -1,5 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'item/dates'
4
+ require_relative 'item/tags'
5
+ require_relative 'item/state'
6
+ require_relative 'item/query'
7
+
3
8
  module Doing
4
9
  ##
5
10
  ## This class describes a single WWID item
@@ -29,57 +34,6 @@ module Doing
29
34
  @note = Note.new(note)
30
35
  end
31
36
 
32
- # def date=(new_date)
33
- # @date = new_date.is_a?(Time) ? new_date : Time.parse(new_date)
34
- # end
35
-
36
- ## If the entry doesn't have a @done date, return the elapsed time
37
- def duration
38
- return nil unless should_time? && should_finish?
39
-
40
- return nil if @title =~ /(?<=^| )@done\b/
41
-
42
- return Time.now - @date
43
- end
44
-
45
- ##
46
- ## Get the difference between the item's start date and
47
- ## the value of its @done tag (if present)
48
- ##
49
- ## @return Interval in seconds
50
- ##
51
- def interval
52
- @interval ||= calc_interval
53
- end
54
-
55
- ##
56
- ## Get the value of the item's @done tag
57
- ##
58
- ## @return [Time] @done value
59
- ##
60
- def end_date
61
- @end_date ||= Time.parse(Regexp.last_match(1)) if @title =~ /@done\((\d{4}-\d\d-\d\d \d\d:\d\d.*?)\)/
62
- end
63
-
64
- def calculate_end_date(opt)
65
- if opt[:took]
66
- if @date + opt[:took] > Time.now
67
- @date = Time.now - opt[:took]
68
- Time.now
69
- else
70
- @date + opt[:took]
71
- end
72
- elsif opt[:back]
73
- if opt[:back].is_a? Integer
74
- @date + opt[:back]
75
- else
76
- @date + (opt[:back] - @date)
77
- end
78
- else
79
- Time.now
80
- end
81
- end
82
-
83
37
  # Generate a hash that represents the entry
84
38
  #
85
39
  # @return [String] entry hash
@@ -107,296 +61,6 @@ module Doing
107
61
  true
108
62
  end
109
63
 
110
- ##
111
- ## Test if two items occur at the same time (same start date and equal duration)
112
- ##
113
- ## @param item_b [Item] The item to compare
114
- ##
115
- ## @return [Boolean] is equal?
116
- ##
117
- def same_time?(item_b)
118
- date == item_b.date ? interval == item_b.interval : false
119
- end
120
-
121
- ##
122
- ## Test if the interval between start date and @done
123
- ## value overlaps with another item's
124
- ##
125
- ## @param item_b [Item] The item to compare
126
- ##
127
- ## @return [Boolean] overlaps?
128
- ##
129
- def overlapping_time?(item_b)
130
- return true if same_time?(item_b)
131
-
132
- start_a = date
133
- a_interval = interval
134
- end_a = a_interval ? start_a + a_interval.to_i : start_a
135
- start_b = item_b.date
136
- b_interval = item_b.interval
137
- end_b = b_interval ? start_b + b_interval.to_i : start_b
138
- (start_a >= start_b && start_a <= end_b) || (end_a >= start_b && end_a <= end_b) || (start_a < start_b && end_a > end_b)
139
- end
140
-
141
- ##
142
- ## Updates the title of the Item by expanding natural
143
- ## language dates within configured date tags (tags
144
- ## whose value is expected to be a date)
145
- ##
146
- ## @param additional_tags An array of additional
147
- ## tag names to consider
148
- ## dates
149
- ##
150
- def expand_date_tags(additional_tags = nil)
151
- @title.expand_date_tags(additional_tags)
152
- end
153
-
154
- ##
155
- ## Add (or remove) tags from the title of the item
156
- ##
157
- ## @param tags [Array] The tags to apply
158
- ## @param options Additional options
159
- ##
160
- ## @option options :date [Boolean] Include timestamp?
161
- ## @option options :single [Boolean] Log as a single change?
162
- ## @option options :value [String] A value to include as @tag(value)
163
- ## @option options :remove [Boolean] if true remove instead of adding
164
- ## @option options :rename_to [String] if not nil, rename target tag to this tag name
165
- ## @option options :regex [Boolean] treat target tag string as regex pattern
166
- ## @option options :force [Boolean] with rename_to, add tag if it doesn't exist
167
- ##
168
- def tag(tags, **options)
169
- added = []
170
- removed = []
171
-
172
- date = options.fetch(:date, false)
173
- options[:value] ||= date ? Time.now.strftime('%F %R') : nil
174
- options.delete(:date)
175
-
176
- single = options.fetch(:single, false)
177
- options.delete(:single)
178
-
179
- tags = tags.to_tags if tags.is_a? ::String
180
-
181
- remove = options.fetch(:remove, false)
182
- tags.each do |tag|
183
- bool = remove ? :and : :not
184
- if tags?(tag, bool)
185
- @title = @title.tag(tag, **options).strip
186
- remove ? removed.push(tag) : added.push(tag)
187
- end
188
- end
189
-
190
- Doing.logger.log_change(tags_added: added, tags_removed: removed, count: 1, item: self, single: single)
191
-
192
- self
193
- end
194
-
195
- ##
196
- ## Get a list of tags on the item
197
- ##
198
- ## @return [Array] array of tags (no values)
199
- ##
200
- def tags
201
- @title.scan(/(?<= |\A)@([^\s(]+)/).map { |tag| tag[0] }.sort.uniq
202
- end
203
-
204
- ##
205
- ## convert tags on item to an array with @ symbols removed
206
- ##
207
- ## @return [Array] array of tags
208
- ##
209
- def tag_array
210
- tags.tags_to_array
211
- end
212
-
213
- ##
214
- ## Test if item contains tag(s)
215
- ##
216
- ## @param tags (Array or String) The tags to test. Can be an array or a comma-separated string.
217
- ## @param bool (Symbol) The boolean to use for multiple tags (:and, :or, :not)
218
- ## @param negate [Boolean] negate the result?
219
- ##
220
- ## @return [Boolean] true if tag/bool combination passes
221
- ##
222
- def tags?(tags, bool = :and, negate: false)
223
- if bool == :pattern
224
- tags = tags.to_tags.tags_to_array.join(' ')
225
- matches = tag_pattern?(tags)
226
-
227
- return negate ? !matches : matches
228
- end
229
-
230
- tags = split_tags(tags)
231
- bool = bool.normalize_bool
232
-
233
- matches = case bool
234
- when :and
235
- all_tags?(tags)
236
- when :not
237
- no_tags?(tags)
238
- else
239
- any_tags?(tags)
240
- end
241
- negate ? !matches : matches
242
- end
243
-
244
- ##
245
- ## Test if item matches tag values
246
- ##
247
- ## @param queries (Array) The tag value queries to test
248
- ## @param bool (Symbol) The boolean to use for multiple tags (:and, :or, :not)
249
- ## @param negate [Boolean] negate the result?
250
- ##
251
- ## @return [Boolean] true if tag/bool combination passes
252
- ##
253
- def tag_values?(queries, bool = :and, negate: false)
254
- bool = bool.normalize_bool
255
-
256
- matches = case bool
257
- when :and
258
- all_values?(queries)
259
- when :not
260
- no_values?(queries)
261
- else
262
- any_values?(queries)
263
- end
264
- negate ? !matches : matches
265
- end
266
-
267
- ##
268
- ## Determine if case should be ignored for searches
269
- ##
270
- ## @param search [String] The search string
271
- ## @param case_type [Symbol] The case type
272
- ##
273
- ## @return [Boolean] case should be ignored
274
- ##
275
- def ignore_case(search, case_type)
276
- (case_type == :smart && search !~ /[A-Z]/) || case_type == :ignore
277
- end
278
-
279
- def highlight_search(search, distance: nil, negate: false, case_type: nil)
280
- prefs = Doing.setting('search', {})
281
- matching = prefs.fetch('matching', 'pattern').normalize_matching
282
- distance ||= prefs.fetch('distance', 3).to_i
283
- case_type ||= prefs.fetch('case', 'smart').normalize_case
284
- new_note = Note.new
285
-
286
- if search.rx? || matching == :fuzzy
287
- rx = search.to_rx(distance: distance, case_type: case_type)
288
- new_title = @title.gsub(rx) { |m| yellow(m) }
289
- new_note.add(@note.to_s.gsub(rx) { |m| yellow(m) })
290
- else
291
- query = search.strip.to_phrase_query
292
-
293
- if query[:must].nil? && query[:must_not].nil?
294
- query[:must] = query[:should]
295
- query[:should] = []
296
- end
297
- query[:must].concat(query[:should]).each do |s|
298
- rx = Regexp.new(s.wildcard_to_rx, ignore_case(s, case_type))
299
- new_title = @title.gsub(rx) { |m| yellow(m) }
300
- new_note.add(@note.to_s.gsub(rx) { |m| yellow(m) })
301
- end
302
- end
303
-
304
- Item.new(@date, new_title, @section, new_note)
305
- end
306
-
307
- ##
308
- ## Test if item matches search string
309
- ##
310
- ## @param search [String] The search string
311
- ## @param negate [Boolean] negate results
312
- ## @param case_type (Symbol) The case-sensitivity
313
- ## type (:sensitive,
314
- ## :ignore, :smart)
315
- ##
316
- ## @return [Boolean] matches search criteria
317
- ##
318
- def search(search, distance: nil, negate: false, case_type: nil)
319
- prefs = Doing.setting('search', {})
320
- matching = prefs.fetch('matching', 'pattern').normalize_matching
321
- distance ||= prefs.fetch('distance', 3).to_i
322
- case_type ||= prefs.fetch('case', 'smart').normalize_case
323
-
324
- if search.rx? || matching == :fuzzy
325
- matches = @title + @note.to_s =~ search.to_rx(distance: distance, case_type: case_type)
326
- else
327
- query = search.strip.to_phrase_query
328
-
329
- if query[:must].nil? && query[:must_not].nil?
330
- query[:must] = query[:should]
331
- query[:should] = []
332
- end
333
- matches = no_searches?(query[:must_not], case_type: case_type)
334
- matches &&= all_searches?(query[:must], case_type: case_type)
335
- matches &&= any_searches?(query[:should], case_type: case_type)
336
- end
337
- # if search =~ /(?<=\A| )[+-]\S/
338
- # else
339
- # text = @title + @note.to_s
340
- # matches = text =~ search.to_rx(distance: distance, case_type: case_type)
341
- # end
342
-
343
- # if search.rx? || !fuzzy
344
- # matches = text =~ search.to_rx(distance: distance, case_type: case_type)
345
- # else
346
- # distance = 0.25 if distance > 1
347
- # score = if (case_type == :smart && search !~ /[A-Z]/) || case_type == :ignore
348
- # text.downcase.pair_distance_similar(search.downcase)
349
- # else
350
- # score = text.pair_distance_similar(search)
351
- # end
352
-
353
- # if score >= distance
354
- # matches = true
355
- # Doing.logger.debug('Fuzzy Match:', %(#{@title}, "#{search}" #{score}))
356
- # end
357
- # end
358
-
359
- negate ? !matches : matches
360
- end
361
-
362
- ##
363
- ## Test if item has a @done tag
364
- ##
365
- ## @return [Boolean] true item has @done tag
366
- ##
367
- def finished?
368
- tags?('done')
369
- end
370
-
371
- ##
372
- ## Test if item does not contain @done tag
373
- ##
374
- ## @return [Boolean] true if item is missing @done tag
375
- ##
376
- def unfinished?
377
- tags?('done', negate: true)
378
- end
379
-
380
- ##
381
- ## Test if item is included in never_finish config and
382
- ## thus should not receive a @done tag
383
- ##
384
- ## @return [Boolean] item should receive @done tag
385
- ##
386
- def should_finish?
387
- should?('never_finish')
388
- end
389
-
390
- ##
391
- ## Test if item is included in never_time config and
392
- ## thus should not receive a date on the @done tag
393
- ##
394
- ## @return [Boolean] item should receive @done date
395
- ##
396
- def should_time?
397
- should?('never_time')
398
- end
399
-
400
64
  ##
401
65
  ## Move item from current section to destination section
402
66
  ##
@@ -449,206 +113,11 @@ module Doing
449
113
  # @private
450
114
  def inspect
451
115
  # %(<Doing::Item @date=#{@date} @title="#{@title}" @section:"#{@section}" @note:#{@note.to_s}>)
452
- %(<Doing::Item @date=#{@date}>)
116
+ %(<Doing::Item @date=#{@date.strftime('%F %T')} @section=#{@section} @title=#{@title.trunc(30)}>)
453
117
  end
454
118
 
455
119
  def clone
456
120
  Marshal.load(Marshal.dump(self))
457
121
  end
458
-
459
- private
460
-
461
- def should?(key)
462
- config = Doing.settings
463
- return true unless config[key].is_a?(Array)
464
-
465
- config[key].each do |tag|
466
- if tag =~ /^@/
467
- return false if tags?(tag.sub(/^@/, '').downcase)
468
- elsif section.downcase == tag.downcase
469
- return false
470
- end
471
- end
472
-
473
- true
474
- end
475
-
476
- def calc_interval
477
- return nil unless should_time? && should_finish?
478
-
479
- done = end_date
480
- return nil if done.nil?
481
-
482
- start = @date
483
-
484
- t = (done - start).to_i
485
- t.positive? ? t : nil
486
- end
487
-
488
- def all_searches?(searches, case_type: :smart)
489
- return true unless searches.good?
490
-
491
- text = @title + @note.to_s
492
- searches.each do |s|
493
- rx = Regexp.new(s.wildcard_to_rx, ignore_case(s, case_type))
494
- return false unless text =~ rx
495
- end
496
- true
497
- end
498
-
499
- def no_searches?(searches, case_type: :smart)
500
- return true unless searches.good?
501
-
502
- text = @title + @note.to_s
503
- searches.each do |s|
504
- rx = Regexp.new(s.wildcard_to_rx, ignore_case(s, case_type))
505
- return false if text =~ rx
506
- end
507
- true
508
- end
509
-
510
- def any_searches?(searches, case_type: :smart)
511
- return true unless searches.good?
512
-
513
- text = @title + @note.to_s
514
- searches.each do |s|
515
- rx = Regexp.new(s.wildcard_to_rx, ignore_case(s, case_type))
516
- return true if text =~ rx
517
- end
518
- false
519
- end
520
-
521
- def all_tags?(tags)
522
- return true unless tags.good?
523
-
524
- tags.each do |tag|
525
- return false unless @title =~ /@#{tag.wildcard_to_rx}(?= |\(|\Z)/i
526
- end
527
- true
528
- end
529
-
530
- def no_tags?(tags)
531
- return true unless tags.good?
532
-
533
- tags.each do |tag|
534
- return false if @title =~ /@#{tag.wildcard_to_rx}(?= |\(|\Z)/i
535
- end
536
- true
537
- end
538
-
539
- def any_tags?(tags)
540
- return true unless tags.good?
541
-
542
- tags.each do |tag|
543
- return true if @title =~ /@#{tag.wildcard_to_rx}(?= |\(|\Z)/i
544
- end
545
- false
546
- end
547
-
548
- def tag_value(tag)
549
- res = @title.match(/@#{tag.sub(/^@/, '').wildcard_to_rx}\((.*?)\)/)
550
- res ? res[1] : nil
551
- end
552
-
553
- def number_or_date(value)
554
- return nil unless value
555
-
556
- if value.strip =~ /^[0-9.]+%?$/
557
- value.strip.to_f
558
- else
559
- value.strip.chronify(guess: :end)
560
- end
561
- end
562
-
563
- def split_value_query(query)
564
- val_rx = /^(!)?@?(\S+) +(!?[<>=][=*]?|[$*^]=) +(.*?)$/
565
- query.match(val_rx)
566
- end
567
-
568
- def any_values?(queries)
569
- return true unless queries.good?
570
-
571
- queries.each do |q|
572
- parts = split_value_query(q)
573
- return true if tag_value_matches?(parts[2], parts[3], parts[4], parts[1])
574
- end
575
- false
576
- end
577
-
578
- def all_values?(queries)
579
- return true unless queries.good?
580
-
581
- queries.each do |q|
582
- parts = split_value_query(q)
583
- return false unless tag_value_matches?(parts[2], parts[3], parts[4], parts[1])
584
- end
585
- true
586
- end
587
-
588
- def no_values?(queries)
589
- return true unless queries.good?
590
-
591
- queries.each do |q|
592
- parts = split_value_query(q)
593
- return false if tag_value_matches?(parts[2], parts[3], parts[4], parts[1])
594
- end
595
- true
596
- end
597
-
598
- def tag_value_matches?(tag, comp, value, negate)
599
- if all_tags?([tag])
600
- tag_val = tag_value(tag)
601
-
602
- if (value.chronify.nil? && value =~ /[a-z]/i && comp =~ /^!?==?$/) || comp =~ /[$*^]=/
603
- is_match = case comp
604
- when /\^=/
605
- tag_val =~ /^#{value.wildcard_to_rx}/i
606
- when /\$=/
607
- tag_val =~ /#{value.wildcard_to_rx}$/i
608
- when %r{==}
609
- tag_val =~ /^#{value.wildcard_to_rx}$/i
610
- else
611
- tag_val =~ /#{value.wildcard_to_rx}/i
612
- end
613
-
614
- comp =~ /!/ || negate ? !is_match : is_match
615
- else
616
- tag_val = number_or_date(tag_val)
617
- val = number_or_date(value)
618
-
619
- return false if val.nil? || tag_val.nil?
620
-
621
- return false unless val.class == tag_val.class
622
-
623
- matches = case comp
624
- when /^<$/
625
- tag_val < val
626
- when /^<=$/
627
- tag_val <= val
628
- when /^>$/
629
- tag_val > val
630
- when /^>=$/
631
- tag_val >= val
632
- when /^!=/
633
- tag_val != val
634
- when /^=/
635
- tag_val == val
636
- end
637
- negate.nil? ? matches : !matches
638
- end
639
- else
640
- false
641
- end
642
- end
643
-
644
- def tag_pattern?(tags)
645
- query = tags.to_query
646
-
647
- no_tags?(query[:must_not]) && all_tags?(query[:must]) && any_tags?(query[:should])
648
- end
649
-
650
- def split_tags(tags)
651
- tags.to_tags.tags_to_array
652
- end
653
122
  end
654
123
  end
data/lib/doing/items.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Doing
4
- # Items Array
4
+ # A collection of Item objects
5
5
  class Items < Array
6
6
  attr_accessor :sections
7
7
 
@@ -27,13 +27,29 @@ module Doing
27
27
  def section?(section)
28
28
  has_section = false
29
29
  section = section.is_a?(Section) ? section.title.downcase : section.downcase
30
- @sections.each do |s|
31
- if s.title.downcase == section
32
- has_section = true
33
- break
34
- end
30
+ @sections.map { |i| i.title.downcase }.include?(section)
31
+ end
32
+
33
+ ##
34
+ ## Return the best section match for a search query
35
+ ##
36
+ ## @param frag The search query
37
+ ## @param distance The distance apart characters can be (fuzziness)
38
+ ##
39
+ ## @return [Section] (first) matching section object
40
+ ##
41
+ def guess_section(frag, distance: 2)
42
+ section = nil
43
+ re = frag.to_rx(distance: distance, case_type: :ignore)
44
+ @sections.each do |sect|
45
+ next unless sect.title =~ /#{re}/i
46
+
47
+ Doing.logger.debug('Match:', %(Assuming "#{sect.title}" from "#{frag}"))
48
+ section = sect
49
+ break
35
50
  end
36
- has_section
51
+
52
+ section
37
53
  end
38
54
 
39
55
  # Add a new section to the sections array. Accepts
@@ -131,17 +147,26 @@ module Doing
131
147
  end
132
148
 
133
149
  ##
134
- ## Return Items containing items that don't exist in receiver
150
+ ## Return Items containing items that don't exist in
151
+ ## receiver
135
152
  ##
136
153
  ## @param items [Items] Receiver
137
154
  ##
155
+ ## @return [Hash] Hash of added and deleted items
156
+ ##
138
157
  def diff(items)
139
- diff = Items.new
140
- each do |item|
141
- res = items.select { |i| i.equal?(item) }
142
- diff.push(item) unless res.count.positive?
158
+ a = clone
159
+ b = items.clone
160
+
161
+ a.delete_if do |item|
162
+ if b.index(item)
163
+ b.delete(item)
164
+ true
165
+ else
166
+ false
167
+ end
143
168
  end
144
- diff
169
+ { deleted: b, added: a }
145
170
  end
146
171
 
147
172
  ##
@@ -179,7 +204,7 @@ module Doing
179
204
  out = []
180
205
  @sections.each do |section|
181
206
  out.push(section.original)
182
- items = in_section(section.title).sort_by(&:date)
207
+ items = in_section(section.title).sort_by { |i| [i.date, i.title] }
183
208
  items.reverse! if Doing.setting('doing_file_sort').normalize_order == :desc
184
209
  items.each { |item| out.push(item.to_s) }
185
210
  end
@@ -84,11 +84,11 @@ module Doing
84
84
  def validate_plugin(title, type, klass)
85
85
  type = valid_type(type)
86
86
  if type == :import && !klass.respond_to?(:import)
87
- raise Errors::PluginUncallable.new('Import plugins must respond to :import', type: type, plugin: title)
87
+ raise Errors::PluginUncallable.new('Import plugins must respond to :import', type, title)
88
88
  end
89
89
 
90
90
  if type == :export && !klass.respond_to?(:render)
91
- raise Errors::PluginUncallable.new('Export plugins must respond to :render', type: type, plugin: title)
91
+ raise Errors::PluginUncallable.new('Export plugins must respond to :render', type, title)
92
92
  end
93
93
 
94
94
  type
@@ -113,7 +113,7 @@ module Doing
113
113
  when /^e(x(p(o(r(t)?)?)?)?)?$/
114
114
  :export
115
115
  else
116
- raise Errors::InvalidPluginType, 'Invalid plugin type'
116
+ raise Errors::InvalidPluginType.new('Invalid plugin type', 'unrecognized')
117
117
  end
118
118
 
119
119
  type.to_sym
@@ -17,7 +17,7 @@ module Doing
17
17
  end
18
18
 
19
19
  def self.render(wwid, items, variables: {})
20
- # Doing.logger.benchmark(:template_render, :start)
20
+ Doing.logger.benchmark(:template_render, :start)
21
21
  return if items.nil?
22
22
 
23
23
  opt = variables[:options]
@@ -126,18 +126,18 @@ module Doing
126
126
 
127
127
  output.gsub!(/\\%/, '%')
128
128
 
129
- output.highlight_search!(opt[:search]) if opt[:template] =~ /^temp/ && opt[:search] && !opt[:not] && opt[:hilite]
129
+ output.highlight_search!(opt[:search]) if opt[:output] =~ /^temp/ && opt[:search] && !opt[:not] && opt[:hilite]
130
130
 
131
131
  out += "#{output}\n"
132
132
  end
133
133
 
134
- # Doing.logger.debug('Template Export:', "#{items.count} items output to template #{opt[:template]}")
134
+ # Doing.logger.debug('Template Export:', "#{items.count} items output to template #{opt[:output]}")
135
135
  if opt[:totals]
136
136
  out += wwid.tag_times(format: Doing.setting('timer_format').to_sym,
137
137
  sort_by: opt[:sort_tags],
138
138
  sort_order: opt[:tag_order])
139
139
  end
140
- # Doing.logger.benchmark(:template_render, :finish)
140
+ Doing.logger.benchmark(:template_render, :finish)
141
141
  out
142
142
  end
143
143