doing 2.1.12 → 2.1.16

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 (93) hide show
  1. checksums.yaml +4 -4
  2. data/.irbrc +1 -0
  3. data/.yardoc/checksums +16 -14
  4. data/.yardoc/object_types +0 -0
  5. data/.yardoc/objects/root.dat +0 -0
  6. data/CHANGELOG.md +67 -0
  7. data/Gemfile.lock +9 -2
  8. data/README.md +56 -19
  9. data/bin/doing +317 -113
  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 +1 -1
  18. data/docs/doc/Doing/Completion.html +1 -1
  19. data/docs/doc/Doing/Configuration.html +7 -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 +337 -14
  31. data/docs/doc/Doing/Items.html +66 -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 +103 -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 +84 -1
  40. data/docs/doc/Doing/Util.html +1 -1
  41. data/docs/doc/Doing/WWID.html +214 -35
  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 +881 -138
  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 +408 -256
  64. data/docs/doc/top-level-namespace.html +2 -2
  65. data/docs/index.md +56 -19
  66. data/doing.gemspec +2 -0
  67. data/doing.rdoc +257 -48
  68. data/example_plugin.rb +2 -4
  69. data/lib/completion/_doing.zsh +31 -27
  70. data/lib/completion/doing.bash +50 -39
  71. data/lib/completion/doing.fish +37 -7
  72. data/lib/doing/array_chronify.rb +57 -0
  73. data/lib/doing/configuration.rb +4 -1
  74. data/lib/doing/item.rb +176 -0
  75. data/lib/doing/log_adapter.rb +1 -1
  76. data/lib/doing/numeric_chronify.rb +40 -0
  77. data/lib/doing/plugins/export/dayone_export.rb +1 -1
  78. data/lib/doing/plugins/export/json_export.rb +2 -2
  79. data/lib/doing/plugins/export/template_export.rb +47 -90
  80. data/lib/doing/plugins/import/calendar_import.rb +13 -1
  81. data/lib/doing/plugins/import/doing_import.rb +12 -1
  82. data/lib/doing/plugins/import/timing_import.rb +13 -1
  83. data/lib/doing/prompt.rb +54 -1
  84. data/lib/doing/string.rb +97 -33
  85. data/lib/doing/string_chronify.rb +112 -14
  86. data/lib/doing/template_string.rb +1 -1
  87. data/lib/doing/time.rb +6 -6
  88. data/lib/doing/util_backup.rb +1 -1
  89. data/lib/doing/version.rb +1 -1
  90. data/lib/doing/wwid.rb +128 -103
  91. data/lib/doing.rb +36 -31
  92. data/lib/examples/plugins/say_export.rb +1 -4
  93. metadata +46 -2
@@ -5,6 +5,7 @@
5
5
  # author: Brett Terpstra
6
6
  # url: https://brettterpstra.com
7
7
  module Doing
8
+ # Template Export
8
9
  class TemplateExport
9
10
  include Doing::Color
10
11
  include Doing::Util
@@ -32,7 +33,7 @@ module Doing
32
33
 
33
34
  placeholders = {}
34
35
 
35
- if (!item.note.empty?) && wwid.config['include_notes']
36
+ if !item.note.empty? && wwid.config['include_notes']
36
37
  note = item.note.map(&:strip).delete_if(&:empty?)
37
38
  note.map! { |line| "#{line.sub(/^\t*/, '')} " }
38
39
 
@@ -42,122 +43,74 @@ module Doing
42
43
  line.simple_wrap(width)
43
44
  # line.chomp.gsub(/(.{1,#{width}})(\s+|\Z)/, "\\1\n")
44
45
  end
45
- note = note.delete_if(&:empty?)
46
+ note.delete_if(&:empty?)
46
47
  end
47
48
  else
48
49
  note = []
49
50
  end
50
51
 
51
- # output.sub!(/%(\d+)?date/) do
52
- # pad = Regexp.last_match(1).to_i
53
- # format("%#{pad}s", item.date.strftime(opt[:format]))
54
- # end
55
52
  placeholders['date'] = item.date.strftime(opt[:format])
56
53
 
57
54
  interval = wwid.get_interval(item, record: true, formatted: false) if opt[:times]
58
55
  if interval
59
- case opt[:interval_format].to_sym
60
- when :human
61
- _d, h, m = wwid.format_time(interval, human: true)
62
- interval = format('%<h> 4dh %<m>02dm', h: h, m: m)
63
- else
64
- d, h, m = wwid.format_time(interval)
65
- interval = format('%<d>02d:%<h>02d:%<m>02d', d: d, h: h, m: m)
66
- end
56
+ interval = case opt[:interval_format].to_sym
57
+ when :human
58
+ interval.time_string(format: :hm)
59
+ else
60
+ interval.time_string(format: :clock)
61
+ end
67
62
  end
68
63
 
69
64
  interval ||= ''
70
- # output.sub!(/%interval/, interval)
71
65
  placeholders['interval'] = interval
72
66
 
73
67
  duration = item.duration if opt[:duration]
74
68
  if duration
75
- case opt[:interval_format].to_sym
76
- when :human
77
- _d, h, m = wwid.format_time(duration, human: true)
78
- duration = format('%<h> 4dh %<m>02dm', h: h, m: m)
79
- else
80
- d, h, m = wwid.format_time(duration)
81
- duration = format('%<d>02d:%<h>02d:%<m>02d', d: d, h: h, m: m)
82
- end
69
+ duration = case opt[:interval_format].to_sym
70
+ when :human
71
+ duration.time_string(format: :hm)
72
+ else
73
+ duration.time_string(format: :clock)
74
+ end
83
75
  end
84
76
  duration ||= ''
85
- # output.sub!(/%duration/, duration)
86
77
  placeholders['duration'] = duration
87
78
 
88
- # output.sub!(/%(\d+)?shortdate/) do
89
- # pad = Regexp.last_match(1) || 13
90
- # format("%#{pad}s", item.date.relative_date)
91
- # end
92
- placeholders['shortdate'] = format("%13s", item.date.relative_date)
93
- # output.sub!(/%section/, item.section) if item.section
79
+ placeholders['shortdate'] = format('%13s', item.date.relative_date)
94
80
  placeholders['section'] = item.section || ''
95
81
  placeholders['title'] = item.title
96
-
97
- # title_rx = /(?mi)%(?<width>-?\d+)?(?:(?<ichar>[ _t])(?<icount>\d+))?(?<prefix>.[ _t]?)?title(?<after>.*?)$/
98
- # title_color = Doing::Color.reset + output.match(/(?mi)^(.*?)(%.*?title)/)[1].last_color
99
-
100
- # title_offset = Doing::Color.uncolor(output).match(title_rx).begin(0)
101
-
102
- # output.sub!(title_rx) do
103
- # m = Regexp.last_match
104
-
105
- # after = m['after']
106
- # pad = m['width'].to_i
107
- # indent = ''
108
- # if m['ichar']
109
- # char = m['ichar'] =~ /t/ ? "\t" : ' '
110
- # indent = char * m['icount'].to_i
111
- # end
112
- # prefix = m['prefix']
113
- # if opt[:wrap_width]&.positive? || pad.positive?
114
- # width = pad.positive? ? pad : opt[:wrap_width]
115
- # item.title.wrap(width, pad: pad, indent: indent, offset: title_offset, prefix: prefix, color: title_color, after: after, reset: reset)
116
- # # flag + item.title.gsub(/(.{#{opt[:wrap_width]}})(?=\s+|\Z)/, "\\1\n ").sub(/\s*$/, '') + reset
117
- # else
118
- # format("%s%#{pad}s%s", prefix, item.title.sub(/\s*$/, ''), after)
119
- # end
120
- # end
121
-
122
-
123
-
124
82
  placeholders['note'] = note
125
83
  placeholders['idnote'] = note.empty? ? '' : "\n#{note.map { |l| "\t\t#{l.strip} " }.join("\n")}"
126
84
  placeholders['odnote'] = note.empty? ? '' : "\n#{note.map { |l| "#{l.strip} " }.join("\n")}"
127
- placeholders['chompnote'] = note.empty? ? '' : note.map { |l| l.gsub(/\n+/, ' ').gsub(/(^\s*|\s*$)/, '').gsub(/\s+/, ' ') }.join(' ')
128
-
129
- # if note.empty?
130
- # output.gsub!(/%(chomp|[io]d|(\^.)?(([ _t]|[^a-z0-9])?\d+)?(.[ _t]?)?)?note/, '')
131
- # else
132
- # output.sub!(/%note/, "\n#{note.map { |l| "\t#{l.strip} " }.join("\n")}")
133
- # output.sub!(/%idnote/, "\n#{note.map { |l| "\t\t#{l.strip} " }.join("\n")}")
134
- # output.sub!(/%odnote/, "\n#{note.map { |l| "#{l.strip} " }.join("\n")}")
135
- # output.sub!(/(?mi)%(?:\^(?<mchar>.))?(?:(?<ichar>[ _t]|[^a-z0-9])?(?<icount>\d+))?(?<prefix>.[ _t]?)?note/) do
136
- # m = Regexp.last_match
137
- # mark = m['mchar'] || ''
138
- # indent = if m['ichar']
139
- # char = m['ichar'] =~ /t/ ? "\t" : ' '
140
- # char * m['icount'].to_i
141
- # else
142
- # ''
143
- # end
144
- # prefix = m['prefix'] || ''
145
- # "\n#{note.map { |l| "#{mark}#{indent}#{prefix}#{l.strip} " }.join("\n")}"
146
- # end
147
-
148
- # output.sub!(/%chompnote/) do
149
- # note.map { |l| l.gsub(/\n+/, ' ').gsub(/(^\s*|\s*$)/, '').gsub(/\s+/, ' ') }.join(' ')
150
- # end
151
- # end
152
85
 
153
- template = opt[:template].dup
154
- template.sub!(/(?i-m)^([\s\S]*?)(%(?:[io]d|(?:\^[\s\S])?(?:(?:[ _t]|[^a-z0-9])?\d+)?(?:[\s\S][ _t]?)?)?note)([\s\S]*?)$/, '\1\3\2')
155
- output = Doing::TemplateString.new(template, placeholders: placeholders, wrap_width: opt[:wrap_width], color: flag, tags_color: opt[:tags_color], reset: reset).colored
86
+ chompnote = []
87
+ unless note.empty?
88
+ chompnote = note.map do |l|
89
+ l.gsub(/\n+/, ' ').gsub(/(^\s*|\s*$)/, '').gsub(/\s+/, ' ')
90
+ end
91
+ end
92
+ placeholders['chompnote'] = chompnote.join(' ')
156
93
 
157
- output.gsub!(/(?<!\\)%hr(_under)?/) do
94
+ template = opt[:template].dup
95
+ note_rx = /(?i-m)(?x:^([\s\S]*?)
96
+ (%(?:[io]d|(?:\^[\s\S])?
97
+ (?:(?:[ _t]|[^a-z0-9])?\d+)?
98
+ (?:[\s\S][ _t]?)?)?note)
99
+ ([\s\S]*?)$)/
100
+ template.sub!(note_rx, '\1\3\2')
101
+ output = Doing::TemplateString.new(template,
102
+ color: flag,
103
+ placeholders: placeholders,
104
+ reset: reset,
105
+ tags_color: opt[:tags_color],
106
+ wrap_width: opt[:wrap_width]).colored
107
+
108
+ output.gsub!(/(?<!\\)%(\S)?hr(_under)?/) do
158
109
  o = ''
159
- `tput cols`.to_i.times do
160
- o += Regexp.last_match(1).nil? ? '-' : '_'
110
+ TTY::Screen.columns.to_i.times do
111
+ char = Regexp.last_match(2).nil? ? '-' : '_'
112
+ char = Regexp.last_match(1).nil? ? char : Regexp.last_match(1)
113
+ o += char
161
114
  end
162
115
  o
163
116
  end
@@ -170,7 +123,11 @@ module Doing
170
123
  end
171
124
 
172
125
  # Doing.logger.debug('Template Export:', "#{items.count} items output to template #{opt[:template]}")
173
- out += wwid.tag_times(format: wwid.config['timer_format'].to_sym, sort_by_name: opt[:sort_tags], sort_order: opt[:tag_order]) if opt[:totals]
126
+ if opt[:totals]
127
+ out += wwid.tag_times(format: wwid.config['timer_format'].to_sym,
128
+ sort_by_name: opt[:sort_tags],
129
+ sort_order: opt[:tag_order])
130
+ end
174
131
  out
175
132
  end
176
133
 
@@ -61,7 +61,19 @@ module Doing
61
61
  title.strip!
62
62
  new_entry = Item.new(start_time, title, section)
63
63
  new_entry.note = entry['notes'].split(/\n/).map(&:chomp) if entry.key?('notes')
64
- new_items.push(new_entry)
64
+
65
+ is_match = true
66
+
67
+ if options[:search]
68
+ is_match = new_entry.search(options[:search], case_type: options[:date], negate: options[:not])
69
+ end
70
+
71
+ if is_match && options[:date_filter]
72
+ is_match = start_time > options[:date_filter][0] && start_time < options[:date_filter][1]
73
+ is_match = options[:not] ? !is_match : is_match
74
+ end
75
+
76
+ new_items.push(new_entry) if is_match
65
77
  end
66
78
  total = new_items.count
67
79
 
@@ -68,7 +68,18 @@ module Doing
68
68
  new_item = Item.new(item.date, title, section)
69
69
  new_item.note = item.note
70
70
 
71
- imported.push(new_item)
71
+ is_match = true
72
+
73
+ if options[:search]
74
+ is_match = new_item.search(options[:search], case_type: options[:case], negate: options[:not])
75
+ end
76
+
77
+ if is_match && options[:date_filter]
78
+ is_match = new_item.date > options[:date_filter][0] && new_item.date < options[:date_filter][1]
79
+ is_match = options[:not] ? !is_match : is_match
80
+ end
81
+
82
+ imported.push(new_item) if is_match
72
83
  end
73
84
 
74
85
  dups = new_items.count - imported.count
@@ -63,7 +63,19 @@ module Doing
63
63
  title.strip!
64
64
  new_item = Item.new(start_time, title, section)
65
65
  new_item.note.add(entry['notes']) if entry.key?('notes')
66
- new_items.push(new_item)
66
+
67
+ is_match = true
68
+
69
+ if options[:search]
70
+ is_match = new_item.search(options[:search], case_type: options[:case], negate: options[:not])
71
+ end
72
+
73
+ if is_match && options[:date_filter]
74
+ is_match = start_time > options[:date_filter][0] && start_time < options[:date_filter][1]
75
+ is_match = options[:not] ? !is_match : is_match
76
+ end
77
+
78
+ new_items.push(new_item) if is_match
67
79
  end
68
80
  total = new_items.count
69
81
  skipped = data.count - total
data/lib/doing/prompt.rb CHANGED
@@ -23,6 +23,59 @@ module Doing
23
23
  $stdin.gets.strip
24
24
  end
25
25
 
26
+ def read_line(prompt: 'Enter text', completions: [], default_response: '')
27
+ return default_response if @default_answer
28
+
29
+ unless completions.empty?
30
+ completions.sort!
31
+ comp = proc { |s| completions.grep(/^#{Regexp.escape(s)}/) }
32
+ Readline.completion_append_character = " "
33
+ Readline.completion_proc = comp
34
+ end
35
+
36
+ begin
37
+ Readline.readline("#{yellow(prompt).sub(/:?$/, ':')} #{reset}", true).strip
38
+ rescue Interrupt
39
+ raise UserCancelled
40
+ end
41
+ end
42
+
43
+ def read_lines(prompt: 'Enter text', completions: [])
44
+ return default_response if @default_answer
45
+
46
+ completions.sort!
47
+ comp = proc { |s| completions.grep(/^#{Regexp.escape(s)}/) }
48
+ Readline.completion_append_character = " "
49
+ Readline.completion_proc = comp
50
+
51
+ puts "#{boldgreen(prompt.sub(/:?$/, ':'))} #{yellow('Hit return for a new line, ')}#{boldwhite('enter a blank line (')}#{boldyellow('return twice')}#{boldwhite(') to end editing')}"
52
+
53
+ res = []
54
+
55
+ begin
56
+ while line = Readline.readline('> ', true)
57
+ break if line.strip.empty?
58
+ res << line.chomp
59
+ end
60
+ rescue Interrupt
61
+ raise UserCancelled
62
+ end
63
+
64
+ res.join("\n").strip
65
+ end
66
+
67
+ def request_lines(prompt: 'Enter text')
68
+ ask_note = []
69
+ reader = TTY::Reader.new(interrupt: -> { raise Errors::UserCancelled }, track_history: false)
70
+ puts "#{boldgreen(prompt.sub(/:?$/, ':'))} #{yellow('Hit return for a new line, ')}#{boldwhite('enter a blank line (')}#{boldyellow('return twice')}#{boldwhite(') to end editing')}"
71
+ loop do
72
+ res = reader.read_line(green('> '))
73
+ break if res.strip.empty?
74
+
75
+ ask_note.push(res)
76
+ end
77
+ ask_note.join("\n").strip
78
+ end
26
79
 
27
80
  ##
28
81
  ## Ask a yes or no question in the terminal
@@ -205,7 +258,7 @@ module Doing
205
258
  out = [
206
259
  format("%#{pad}d", i),
207
260
  ') ',
208
- format('%13s', item.date.relative_date),
261
+ format('%16s', item.date.strftime('%Y-%m-%d %H:%M')),
209
262
  ' | ',
210
263
  item.title
211
264
  ]
data/lib/doing/string.rb CHANGED
@@ -259,7 +259,13 @@ module Doing
259
259
  end
260
260
  end
261
261
 
262
- def pluralize(number)
262
+ ##
263
+ ## Pluralize a string based on quantity
264
+ ##
265
+ ## @param number [Integer] the quantity of the
266
+ ## object the string represents
267
+ ##
268
+ def to_p(number)
263
269
  number == 1 ? self : "#{self}s"
264
270
  end
265
271
 
@@ -268,10 +274,6 @@ module Doing
268
274
  ##
269
275
  ## @return [Symbol] :oldest or :newest
270
276
  ##
271
- def normalize_age!(default = :newest)
272
- replace normalize_age(default)
273
- end
274
-
275
277
  def normalize_age(default = :newest)
276
278
  case self
277
279
  when /^o/i
@@ -283,6 +285,11 @@ module Doing
283
285
  end
284
286
  end
285
287
 
288
+ ## @see #normalize_age
289
+ def normalize_age!(default = :newest)
290
+ replace normalize_age(default)
291
+ end
292
+
286
293
  ##
287
294
  ## Convert a sort order string to a qualified type
288
295
  ##
@@ -308,10 +315,6 @@ module Doing
308
315
  ##
309
316
  ## @return Symbol :smart, :sensitive, :ignore
310
317
  ##
311
- def normalize_case!
312
- replace normalize_case
313
- end
314
-
315
318
  def normalize_case(default = :smart)
316
319
  case self
317
320
  when /^(c|sens)/i
@@ -325,15 +328,16 @@ module Doing
325
328
  end
326
329
  end
327
330
 
331
+ ## @see #normalize_case
332
+ def normalize_case!
333
+ replace normalize_case
334
+ end
335
+
328
336
  ##
329
337
  ## Convert a boolean string to a symbol
330
338
  ##
331
339
  ## @return Symbol :and, :or, or :not
332
340
  ##
333
- def normalize_bool!(default = :and)
334
- replace normalize_bool(default)
335
- end
336
-
337
341
  def normalize_bool(default = :and)
338
342
  case self
339
343
  when /(and|all)/i
@@ -349,15 +353,19 @@ module Doing
349
353
  end
350
354
  end
351
355
 
356
+ ## @see #normalize_bool
357
+ def normalize_bool!(default = :and)
358
+ replace normalize_bool(default)
359
+ end
360
+
352
361
  ##
353
362
  ## Convert a matching configuration string to a symbol
354
363
  ##
364
+ ## @param default [Symbol] the default matching
365
+ ## type to return if the string
366
+ ## doesn't match a known symbol
355
367
  ## @return Symbol :fuzzy, :pattern, :exact
356
368
  ##
357
- def normalize_matching!(default = :pattern)
358
- replace normalize_bool(default)
359
- end
360
-
361
369
  def normalize_matching(default = :pattern)
362
370
  case self
363
371
  when /^f/i
@@ -371,30 +379,64 @@ module Doing
371
379
  end
372
380
  end
373
381
 
374
- def normalize_trigger!
375
- replace normalize_trigger
382
+ ## @see #normalize_matching
383
+ def normalize_matching!(default = :pattern)
384
+ replace normalize_bool(default)
376
385
  end
377
386
 
387
+ ##
388
+ ## Adds ?: to any parentheticals in a regular expression
389
+ ## to avoid match groups
390
+ ##
391
+ ## @return [String] modified regular expression
392
+ ##
378
393
  def normalize_trigger
379
394
  gsub(/\((?!\?:)/, '(?:').downcase
380
395
  end
381
396
 
397
+ ## @see #normalize_trigger
398
+ def normalize_trigger!
399
+ replace normalize_trigger
400
+ end
401
+
402
+ ##
403
+ ## Convert ? and * wildcards to regular expressions.
404
+ ## Uses \S (non-whitespace) instead of . (any character)
405
+ ##
406
+ ## @return [String] Regular expression string
407
+ ##
382
408
  def wildcard_to_rx
383
409
  gsub(/\?/, '\S').gsub(/\*/, '\S*?')
384
410
  end
385
411
 
412
+ ##
413
+ ## Add @ prefix to string if needed, maintains +/- prefix
414
+ ##
415
+ ## @return [String] @string
416
+ ##
386
417
  def add_at
387
418
  strip.sub(/^([+-]*)@/, '\1')
388
419
  end
389
420
 
421
+ ##
422
+ ## Convert a list of tags to an array. Tags can be with
423
+ ## or without @ symbols, separated by any character, and
424
+ ## can include parenthetical values (with spaces)
425
+ ##
426
+ ## @return [Array] array of tags including @ symbols
427
+ ##
390
428
  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)
429
+ gsub(/ *, */, ' ').scan(/(@?(?:\S+(?:\(.+\)))|@?(?:\S+))/).map(&:first).sort.uniq.map(&:add_at)
396
430
  end
397
431
 
432
+ ##
433
+ ## @brief Adds tags to a string
434
+ ##
435
+ ## @param tags [String or Array] List of tags to add. @ symbol optional
436
+ ## @param remove [Boolean] remove tags instead of adding
437
+ ##
438
+ ## @return [String] the tagged string
439
+ ##
398
440
  def add_tags(tags, remove: false)
399
441
  title = self.dup
400
442
  tags = tags.to_tags
@@ -402,6 +444,11 @@ module Doing
402
444
  title
403
445
  end
404
446
 
447
+ ## @see #add_tags
448
+ def add_tags!(tags, remove: false)
449
+ replace add_tags(tags, remove: remove)
450
+ end
451
+
405
452
  ##
406
453
  ## Add, rename, or remove a tag in place
407
454
  ##
@@ -484,10 +531,6 @@ module Doing
484
531
  ##
485
532
  ## @return Deduplicated string
486
533
  ##
487
- def dedup_tags!
488
- replace dedup_tags
489
- end
490
-
491
534
  def dedup_tags
492
535
  title = dup
493
536
  tags = title.scan(/(?<=\A| )(@(\S+?)(\([^)]+\))?)(?= |\Z)/).uniq
@@ -505,6 +548,11 @@ module Doing
505
548
  title
506
549
  end
507
550
 
551
+ ## @see #dedup_tags
552
+ def dedup_tags!
553
+ replace dedup_tags
554
+ end
555
+
508
556
  # Returns the last escape sequence from a string.
509
557
  #
510
558
  # Actually returns all escape codes, with the assumption
@@ -525,11 +573,9 @@ module Doing
525
573
  ##
526
574
  ## @param opt [Hash] Additional Options
527
575
  ##
528
- def link_urls!(**opt)
529
- fmt = opt.fetch(:format, :html)
530
- replace link_urls(format: fmt)
531
- end
532
-
576
+ ## @option opt [Symbol] :format can be :markdown or
577
+ ## :html (default)
578
+ ##
533
579
  def link_urls(**opt)
534
580
  fmt = opt.fetch(:format, :html)
535
581
  return self unless fmt
@@ -541,6 +587,12 @@ module Doing
541
587
  str.replace_qualified_urls(format: fmt).clean_unlinked_urls
542
588
  end
543
589
 
590
+ ## @see #link_urls
591
+ def link_urls!(**opt)
592
+ fmt = opt.fetch(:format, :html)
593
+ replace link_urls(format: fmt)
594
+ end
595
+
544
596
  # Remove <self-linked> formatting
545
597
  def remove_self_links
546
598
  gsub(/<(.*?)>/) do |match|
@@ -590,6 +642,18 @@ module Doing
590
642
  end
591
643
  end
592
644
 
645
+ ##
646
+ ## Convert a string value to an appropriate type. If
647
+ ## kind is not specified, '[one, two]' becomes an Array,
648
+ ## '1' becomes Integer, '1.5' becomes Float, 'true' or
649
+ ## 'yes' becomes TrueClass, 'false' or 'no' becomes
650
+ ## FalseClass.
651
+ ##
652
+ ## @param kind [String] specify string, array,
653
+ ## integer, float, symbol, or boolean
654
+ ## (falls back to string if value is
655
+ ## not recognized)
656
+ ## @return Converted object type
593
657
  def set_type(kind = nil)
594
658
  if kind
595
659
  case kind.to_s