doing 2.1.14 → 2.1.18

Sign up to get free protection for your applications and to get access to all the features.
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