doing 2.1.12 → 2.1.16

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