doing 2.1.14 → 2.1.18

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 (91) hide show
  1. checksums.yaml +4 -4
  2. data/.irbrc +1 -0
  3. data/.yardoc/checksums +14 -12
  4. data/.yardoc/object_types +0 -0
  5. data/.yardoc/objects/root.dat +0 -0
  6. data/CHANGELOG.md +66 -0
  7. data/Gemfile.lock +3 -2
  8. data/README.md +56 -19
  9. data/bin/doing +134 -47
  10. data/docs/doc/Array.html +117 -3
  11. data/docs/doc/BooleanTermParser/Clause.html +1 -1
  12. data/docs/doc/BooleanTermParser/Operator.html +1 -1
  13. data/docs/doc/BooleanTermParser/Query.html +1 -1
  14. data/docs/doc/BooleanTermParser/QueryParser.html +1 -1
  15. data/docs/doc/BooleanTermParser/QueryTransformer.html +1 -1
  16. data/docs/doc/BooleanTermParser.html +1 -1
  17. data/docs/doc/Doing/Color.html +6 -2
  18. data/docs/doc/Doing/Completion.html +1 -1
  19. data/docs/doc/Doing/Configuration.html +8 -4
  20. data/docs/doc/Doing/Errors/DoingNoTraceError.html +1 -1
  21. data/docs/doc/Doing/Errors/DoingRuntimeError.html +1 -1
  22. data/docs/doc/Doing/Errors/DoingStandardError.html +1 -1
  23. data/docs/doc/Doing/Errors/EmptyInput.html +1 -1
  24. data/docs/doc/Doing/Errors/NoResults.html +1 -1
  25. data/docs/doc/Doing/Errors/PluginException.html +1 -1
  26. data/docs/doc/Doing/Errors/UserCancelled.html +1 -1
  27. data/docs/doc/Doing/Errors/WrongCommand.html +1 -1
  28. data/docs/doc/Doing/Errors.html +1 -1
  29. data/docs/doc/Doing/Hooks.html +1 -1
  30. data/docs/doc/Doing/Item.html +224 -2
  31. data/docs/doc/Doing/Items.html +2 -2
  32. data/docs/doc/Doing/LogAdapter.html +1 -1
  33. data/docs/doc/Doing/Note.html +2 -2
  34. data/docs/doc/Doing/Pager.html +1 -1
  35. data/docs/doc/Doing/Plugins.html +1 -1
  36. data/docs/doc/Doing/Prompt.html +69 -1
  37. data/docs/doc/Doing/Section.html +1 -1
  38. data/docs/doc/Doing/TemplateString.html +2 -2
  39. data/docs/doc/Doing/Util/Backup.html +1 -1
  40. data/docs/doc/Doing/Util.html +1 -1
  41. data/docs/doc/Doing/WWID.html +71 -67
  42. data/docs/doc/Doing.html +3 -3
  43. data/docs/doc/GLI/Commands/MarkdownDocumentListener.html +1 -1
  44. data/docs/doc/GLI/Commands.html +1 -1
  45. data/docs/doc/GLI.html +1 -1
  46. data/docs/doc/Hash.html +1 -1
  47. data/docs/doc/Numeric.html +279 -0
  48. data/docs/doc/PhraseParser/Operator.html +1 -1
  49. data/docs/doc/PhraseParser/PhraseClause.html +1 -1
  50. data/docs/doc/PhraseParser/Query.html +1 -1
  51. data/docs/doc/PhraseParser/QueryParser.html +1 -1
  52. data/docs/doc/PhraseParser/QueryTransformer.html +1 -1
  53. data/docs/doc/PhraseParser/TermClause.html +1 -1
  54. data/docs/doc/PhraseParser.html +1 -1
  55. data/docs/doc/Status.html +1 -1
  56. data/docs/doc/String.html +997 -118
  57. data/docs/doc/Symbol.html +1 -1
  58. data/docs/doc/Time.html +1 -1
  59. data/docs/doc/_index.html +14 -9
  60. data/docs/doc/class_list.html +1 -1
  61. data/docs/doc/file.README.html +41 -15
  62. data/docs/doc/index.html +41 -15
  63. data/docs/doc/method_list.html +448 -312
  64. data/docs/doc/top-level-namespace.html +2 -2
  65. data/docs/index.md +56 -19
  66. data/doing.gemspec +1 -0
  67. data/doing.rdoc +36 -6
  68. data/example_plugin.rb +2 -4
  69. data/lib/completion/_doing.zsh +8 -8
  70. data/lib/completion/doing.bash +12 -12
  71. data/lib/completion/doing.fish +8 -3
  72. data/lib/doing/array_chronify.rb +57 -0
  73. data/lib/doing/colors.rb +4 -0
  74. data/lib/doing/configuration.rb +6 -2
  75. data/lib/doing/item.rb +83 -0
  76. data/lib/doing/log_adapter.rb +3 -3
  77. data/lib/doing/numeric_chronify.rb +40 -0
  78. data/lib/doing/plugins/export/dayone_export.rb +1 -1
  79. data/lib/doing/plugins/export/json_export.rb +2 -2
  80. data/lib/doing/plugins/export/template_export.rb +49 -90
  81. data/lib/doing/prompt.rb +52 -0
  82. data/lib/doing/string.rb +137 -33
  83. data/lib/doing/string_chronify.rb +112 -14
  84. data/lib/doing/template_string.rb +1 -1
  85. data/lib/doing/time.rb +4 -4
  86. data/lib/doing/util_backup.rb +1 -1
  87. data/lib/doing/version.rb +1 -1
  88. data/lib/doing/wwid.rb +107 -101
  89. data/lib/doing.rb +35 -31
  90. data/lib/examples/plugins/say_export.rb +1 -4
  91. metadata +26 -2
data/lib/doing/string.rb CHANGED
@@ -101,6 +101,46 @@ module Doing
101
101
  gsub(/(\s|m)(@[^ ("']+)/, "\\1#{tag_color}\\2#{last_color}")
102
102
  end
103
103
 
104
+ def to_phrase_query(query)
105
+ parser = PhraseParser::QueryParser.new
106
+ transformer = PhraseParser::QueryTransformer.new
107
+ parse_tree = parser.parse(query)
108
+ transformer.apply(parse_tree).to_elasticsearch
109
+ end
110
+
111
+ def ignore_case(search, case_type)
112
+ (case_type == :smart && search !~ /[A-Z]/) || case_type == :ignore
113
+ end
114
+
115
+ def highlight_search!(search, distance: nil, negate: false, case_type: nil)
116
+ replace highlight_search(search, distance: distance, negate: negate, case_type: case_type)
117
+ end
118
+
119
+ def highlight_search(search, distance: nil, negate: false, case_type: nil)
120
+ out = dup
121
+ prefs = Doing.config.settings['search'] || {}
122
+ matching = prefs.fetch('matching', 'pattern').normalize_matching
123
+ distance ||= prefs.fetch('distance', 3).to_i
124
+ case_type ||= prefs.fetch('case', 'smart').normalize_case
125
+
126
+ if search.is_rx? || matching == :fuzzy
127
+ rx = search.to_rx(distance: distance, case_type: case_type)
128
+ out.gsub!(rx) { |m| m.bgyellow.black }
129
+ else
130
+ query = to_phrase_query(search.strip)
131
+
132
+ if query[:must].nil? && query[:must_not].nil?
133
+ query[:must] = query[:should]
134
+ query[:should] = []
135
+ end
136
+ query[:must].concat(query[:should]).each do |s|
137
+ rx = Regexp.new(s.wildcard_to_rx, ignore_case(s, case_type))
138
+ out.gsub!(rx) { |m| m.bgyellow.black }
139
+ end
140
+ end
141
+ out
142
+ end
143
+
104
144
  ##
105
145
  ## Test if line should be ignored
106
146
  ##
@@ -259,7 +299,13 @@ module Doing
259
299
  end
260
300
  end
261
301
 
262
- def pluralize(number)
302
+ ##
303
+ ## Pluralize a string based on quantity
304
+ ##
305
+ ## @param number [Integer] the quantity of the
306
+ ## object the string represents
307
+ ##
308
+ def to_p(number)
263
309
  number == 1 ? self : "#{self}s"
264
310
  end
265
311
 
@@ -268,10 +314,6 @@ module Doing
268
314
  ##
269
315
  ## @return [Symbol] :oldest or :newest
270
316
  ##
271
- def normalize_age!(default = :newest)
272
- replace normalize_age(default)
273
- end
274
-
275
317
  def normalize_age(default = :newest)
276
318
  case self
277
319
  when /^o/i
@@ -283,6 +325,11 @@ module Doing
283
325
  end
284
326
  end
285
327
 
328
+ ## @see #normalize_age
329
+ def normalize_age!(default = :newest)
330
+ replace normalize_age(default)
331
+ end
332
+
286
333
  ##
287
334
  ## Convert a sort order string to a qualified type
288
335
  ##
@@ -308,10 +355,6 @@ module Doing
308
355
  ##
309
356
  ## @return Symbol :smart, :sensitive, :ignore
310
357
  ##
311
- def normalize_case!
312
- replace normalize_case
313
- end
314
-
315
358
  def normalize_case(default = :smart)
316
359
  case self
317
360
  when /^(c|sens)/i
@@ -325,15 +368,16 @@ module Doing
325
368
  end
326
369
  end
327
370
 
371
+ ## @see #normalize_case
372
+ def normalize_case!
373
+ replace normalize_case
374
+ end
375
+
328
376
  ##
329
377
  ## Convert a boolean string to a symbol
330
378
  ##
331
379
  ## @return Symbol :and, :or, or :not
332
380
  ##
333
- def normalize_bool!(default = :and)
334
- replace normalize_bool(default)
335
- end
336
-
337
381
  def normalize_bool(default = :and)
338
382
  case self
339
383
  when /(and|all)/i
@@ -349,15 +393,19 @@ module Doing
349
393
  end
350
394
  end
351
395
 
396
+ ## @see #normalize_bool
397
+ def normalize_bool!(default = :and)
398
+ replace normalize_bool(default)
399
+ end
400
+
352
401
  ##
353
402
  ## Convert a matching configuration string to a symbol
354
403
  ##
404
+ ## @param default [Symbol] the default matching
405
+ ## type to return if the string
406
+ ## doesn't match a known symbol
355
407
  ## @return Symbol :fuzzy, :pattern, :exact
356
408
  ##
357
- def normalize_matching!(default = :pattern)
358
- replace normalize_bool(default)
359
- end
360
-
361
409
  def normalize_matching(default = :pattern)
362
410
  case self
363
411
  when /^f/i
@@ -371,30 +419,64 @@ module Doing
371
419
  end
372
420
  end
373
421
 
374
- def normalize_trigger!
375
- replace normalize_trigger
422
+ ## @see #normalize_matching
423
+ def normalize_matching!(default = :pattern)
424
+ replace normalize_bool(default)
376
425
  end
377
426
 
427
+ ##
428
+ ## Adds ?: to any parentheticals in a regular expression
429
+ ## to avoid match groups
430
+ ##
431
+ ## @return [String] modified regular expression
432
+ ##
378
433
  def normalize_trigger
379
434
  gsub(/\((?!\?:)/, '(?:').downcase
380
435
  end
381
436
 
437
+ ## @see #normalize_trigger
438
+ def normalize_trigger!
439
+ replace normalize_trigger
440
+ end
441
+
442
+ ##
443
+ ## Convert ? and * wildcards to regular expressions.
444
+ ## Uses \S (non-whitespace) instead of . (any character)
445
+ ##
446
+ ## @return [String] Regular expression string
447
+ ##
382
448
  def wildcard_to_rx
383
449
  gsub(/\?/, '\S').gsub(/\*/, '\S*?')
384
450
  end
385
451
 
452
+ ##
453
+ ## Add @ prefix to string if needed, maintains +/- prefix
454
+ ##
455
+ ## @return [String] @string
456
+ ##
386
457
  def add_at
387
458
  strip.sub(/^([+-]*)@/, '\1')
388
459
  end
389
460
 
461
+ ##
462
+ ## Convert a list of tags to an array. Tags can be with
463
+ ## or without @ symbols, separated by any character, and
464
+ ## can include parenthetical values (with spaces)
465
+ ##
466
+ ## @return [Array] array of tags including @ symbols
467
+ ##
390
468
  def to_tags
391
- gsub(/ *, */, ' ').gsub(/ +/, ' ').split(/ /).sort.uniq.map(&:add_at)
392
- end
393
-
394
- def add_tags!(tags, remove: false)
395
- replace add_tags(tags, remove: remove)
469
+ gsub(/ *, */, ' ').scan(/(@?(?:\S+(?:\(.+\)))|@?(?:\S+))/).map(&:first).sort.uniq.map(&:add_at)
396
470
  end
397
471
 
472
+ ##
473
+ ## @brief Adds tags to a string
474
+ ##
475
+ ## @param tags [String or Array] List of tags to add. @ symbol optional
476
+ ## @param remove [Boolean] remove tags instead of adding
477
+ ##
478
+ ## @return [String] the tagged string
479
+ ##
398
480
  def add_tags(tags, remove: false)
399
481
  title = self.dup
400
482
  tags = tags.to_tags
@@ -402,6 +484,11 @@ module Doing
402
484
  title
403
485
  end
404
486
 
487
+ ## @see #add_tags
488
+ def add_tags!(tags, remove: false)
489
+ replace add_tags(tags, remove: remove)
490
+ end
491
+
405
492
  ##
406
493
  ## Add, rename, or remove a tag in place
407
494
  ##
@@ -484,10 +571,6 @@ module Doing
484
571
  ##
485
572
  ## @return Deduplicated string
486
573
  ##
487
- def dedup_tags!
488
- replace dedup_tags
489
- end
490
-
491
574
  def dedup_tags
492
575
  title = dup
493
576
  tags = title.scan(/(?<=\A| )(@(\S+?)(\([^)]+\))?)(?= |\Z)/).uniq
@@ -505,6 +588,11 @@ module Doing
505
588
  title
506
589
  end
507
590
 
591
+ ## @see #dedup_tags
592
+ def dedup_tags!
593
+ replace dedup_tags
594
+ end
595
+
508
596
  # Returns the last escape sequence from a string.
509
597
  #
510
598
  # Actually returns all escape codes, with the assumption
@@ -525,11 +613,9 @@ module Doing
525
613
  ##
526
614
  ## @param opt [Hash] Additional Options
527
615
  ##
528
- def link_urls!(**opt)
529
- fmt = opt.fetch(:format, :html)
530
- replace link_urls(format: fmt)
531
- end
532
-
616
+ ## @option opt [Symbol] :format can be :markdown or
617
+ ## :html (default)
618
+ ##
533
619
  def link_urls(**opt)
534
620
  fmt = opt.fetch(:format, :html)
535
621
  return self unless fmt
@@ -541,6 +627,12 @@ module Doing
541
627
  str.replace_qualified_urls(format: fmt).clean_unlinked_urls
542
628
  end
543
629
 
630
+ ## @see #link_urls
631
+ def link_urls!(**opt)
632
+ fmt = opt.fetch(:format, :html)
633
+ replace link_urls(format: fmt)
634
+ end
635
+
544
636
  # Remove <self-linked> formatting
545
637
  def remove_self_links
546
638
  gsub(/<(.*?)>/) do |match|
@@ -590,6 +682,18 @@ module Doing
590
682
  end
591
683
  end
592
684
 
685
+ ##
686
+ ## Convert a string value to an appropriate type. If
687
+ ## kind is not specified, '[one, two]' becomes an Array,
688
+ ## '1' becomes Integer, '1.5' becomes Float, 'true' or
689
+ ## 'yes' becomes TrueClass, 'false' or 'no' becomes
690
+ ## FalseClass.
691
+ ##
692
+ ## @param kind [String] specify string, array,
693
+ ## integer, float, symbol, or boolean
694
+ ## (falls back to string if value is
695
+ ## not recognized)
696
+ ## @return Converted object type
593
697
  def set_type(kind = nil)
594
698
  if kind
595
699
  case kind.to_s
@@ -64,22 +64,120 @@ module Doing
64
64
  when /^(\d+):(\d\d)$/
65
65
  minutes += Regexp.last_match(1).to_i * 60
66
66
  minutes += Regexp.last_match(2).to_i
67
- when /^(\d+(?:\.\d+)?)([hmd])?$/
68
- amt = Regexp.last_match(1)
69
- type = Regexp.last_match(2).nil? ? 'm' : Regexp.last_match(2)
70
-
71
- minutes = case type.downcase
72
- when 'm'
73
- amt.to_i
74
- when 'h'
75
- (amt.to_f * 60).round
76
- when 'd'
77
- (amt.to_f * 60 * 24).round
78
- else
79
- minutes
80
- end
67
+ when /^(\d+(?:\.\d+)?)([hmd])?/
68
+ scan(/(\d+(?:\.\d+)?)([hmd])?/).each do |m|
69
+ amt = m[0]
70
+ type = m[1].nil? ? 'm' : m[1]
71
+
72
+ minutes += case type.downcase
73
+ when 'm'
74
+ amt.to_i
75
+ when 'h'
76
+ (amt.to_f * 60).round
77
+ when 'd'
78
+ (amt.to_f * 60 * 24).round
79
+ else
80
+ 0
81
+ end
82
+ end
81
83
  end
82
84
  minutes * 60
83
85
  end
86
+
87
+ ##
88
+ ## Convert DD:HH:MM to seconds
89
+ ##
90
+ ## @return [Integer] rounded number of seconds
91
+ ##
92
+ def to_seconds
93
+ mtch = match(/(\d+):(\d+):(\d+)/)
94
+
95
+ raise Errors::DoingRuntimeError, "Invalid time string: #{self}" unless mtch
96
+
97
+ h = mtch[1]
98
+ m = mtch[2]
99
+ s = mtch[3]
100
+ (h.to_i * 60 * 60) + (m.to_i * 60) + s.to_i
101
+ end
102
+
103
+ ##
104
+ ## Convert DD:HH:MM to a natural language string
105
+ ##
106
+ ## @param format [Symbol] The format to output (:dhm, :hm, :m, :clock, :natural)
107
+ ##
108
+ def time_string(format: :dhm)
109
+ to_seconds.time_string(format: format)
110
+ end
111
+
112
+ ##
113
+ ## Convert (chronify) natural language dates
114
+ ## within configured date tags (tags whose value is
115
+ ## expected to be a date). Modifies string in place.
116
+ ##
117
+ ## @param additional_tags [Array] An array of
118
+ ## additional tags to
119
+ ## consider date_tags
120
+ ##
121
+ def expand_date_tags(additional_tags = nil)
122
+ iso_rx = /\d{4}-\d\d-\d\d \d\d:\d\d/
123
+
124
+ watch_tags = [
125
+ 'start(?:ed)?',
126
+ 'beg[ia]n',
127
+ 'done',
128
+ 'finished',
129
+ 'completed?',
130
+ 'waiting',
131
+ 'defer(?:red)?'
132
+ ]
133
+
134
+ if additional_tags
135
+ date_tags = additional_tags
136
+ date_tags = date_tags.split(/ *, */) if date_tags.is_a?(String)
137
+ date_tags.map! do |tag|
138
+ tag.sub(/^@/, '').gsub(/\((?!\?:)(.*?)\)/, '(?:\1)').strip
139
+ end
140
+ watch_tags.concat(date_tags).uniq!
141
+ end
142
+
143
+ done_rx = /(?<=^| )@(?<tag>#{watch_tags.join('|')})\((?<date>.*?)\)/i
144
+
145
+ gsub!(done_rx) do
146
+ m = Regexp.last_match
147
+ t = m['tag']
148
+ d = m['date']
149
+ future = t =~ /^(done|complete)/ ? false : true
150
+ parsed_date = d =~ iso_rx ? Time.parse(d) : d.chronify(guess: :begin, future: future)
151
+ parsed_date.nil? ? m[0] : "@#{t}(#{parsed_date.strftime('%F %R')})"
152
+ end
153
+ end
154
+
155
+ ##
156
+ ## Splits a range string and returns an array of
157
+ ## DateTime objects as [start, end]. If only one date is
158
+ ## given, end time is nil.
159
+ ##
160
+ ## @return [Array<DateTime>] Start and end dates as
161
+ ## array
162
+ ## @example Process a natural language date range
163
+ ## "mon 3pm to mon 5pm".split_date_range
164
+ ##
165
+ def split_date_range
166
+ date_string = dup
167
+ case date_string
168
+ when / (to|through|thru|(un)?til|-+) /
169
+ dates = date_string.split(/ (?:to|through|thru|(?:un)?til|-+) /)
170
+ start = dates[0].chronify(guess: :begin)
171
+ finish = dates[-1].chronify(guess: :end)
172
+ else
173
+ start = date_string.chronify(guess: :begin)
174
+ finish = nil
175
+ end
176
+
177
+ raise InvalidTimeExpression, 'Unrecognized date string' unless start
178
+
179
+ Doing.logger.debug('Parser:', "date range interpreted as #{start.strftime('%F %R')} -- #{finish ? finish.strftime('%F %R') : 'now'}")
180
+ [start, finish]
181
+ end
84
182
  end
85
183
  end
@@ -176,7 +176,7 @@ module Doing
176
176
  ' '
177
177
  else
178
178
  line = l.gsub(/%/, '\%').strip.wrap(width, pad: pad, indent: indent, offset: 0, prefix: prefix, color: last_color, after: after, reset: reset, pad_first: true)
179
- line.highlight_tags!(tags_color, last_color: last_color) unless tags_color.nil? || tags_color.empty?
179
+ line.highlight_tags!(tags_color, last_color: last_color) unless !tags_color || tags_color.nil? || tags_color.empty?
180
180
  "#{line} "
181
181
  end
182
182
  end.join("\n")
data/lib/doing/time.rb CHANGED
@@ -25,10 +25,10 @@ module Doing
25
25
  h = h % 24
26
26
 
27
27
  output = []
28
- output.push("#{d} #{'day'.pluralize(d)}") if d.positive?
29
- output.push("#{h} #{'hour'.pluralize(h)}") if h.positive?
30
- output.push("#{m} #{'minute'.pluralize(m)}") if m.positive?
31
- output.push("#{s} #{'second'.pluralize(s)}") if s.positive?
28
+ output.push("#{d} #{'day'.to_p(d)}") if d.positive?
29
+ output.push("#{h} #{'hour'.to_p(h)}") if h.positive?
30
+ output.push("#{m} #{'minute'.to_p(m)}") if m.positive?
31
+ output.push("#{s} #{'second'.to_p(s)}") if s.positive?
32
32
  output.join(', ')
33
33
  end
34
34
 
@@ -79,7 +79,7 @@ module Doing
79
79
  def redo_backup(filename = nil, count: 1)
80
80
  filename ||= Doing.config.settings['doing_file']
81
81
  # redo_file = File.join(backup_dir, "undone___#{File.basename(filename)}")
82
- undones = Dir.glob("undone*#{File.basename(filename)}", base: backup_dir).sort
82
+ undones = Dir.glob("undone*#{File.basename(filename)}", base: backup_dir).sort.reverse
83
83
  total = undones.count
84
84
  count = total if count > total
85
85
 
data/lib/doing/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Doing
2
- VERSION = '2.1.14'
2
+ VERSION = '2.1.18'
3
3
  end