doing 2.1.13 → 2.1.17

Sign up to get free protection for your applications and to get access to all the features.
Files changed (94) 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 +76 -0
  7. data/Gemfile.lock +9 -2
  8. data/README.md +56 -19
  9. data/bin/doing +218 -68
  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 +340 -14
  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 +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 +1 -1
  40. data/docs/doc/Doing/Util.html +1 -1
  41. data/docs/doc/Doing/WWID.html +77 -71
  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 +449 -305
  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 +76 -9
  68. data/example_plugin.rb +2 -4
  69. data/lib/completion/_doing.zsh +17 -17
  70. data/lib/completion/doing.bash +25 -25
  71. data/lib/completion/doing.fish +18 -6
  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 +108 -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/plugins/import/calendar_import.rb +13 -1
  82. data/lib/doing/plugins/import/doing_import.rb +12 -1
  83. data/lib/doing/plugins/import/timing_import.rb +13 -1
  84. data/lib/doing/prompt.rb +65 -1
  85. data/lib/doing/string.rb +137 -33
  86. data/lib/doing/string_chronify.rb +112 -14
  87. data/lib/doing/template_string.rb +1 -1
  88. data/lib/doing/time.rb +6 -6
  89. data/lib/doing/util_backup.rb +1 -1
  90. data/lib/doing/version.rb +1 -1
  91. data/lib/doing/wwid.rb +117 -106
  92. data/lib/doing.rb +36 -31
  93. data/lib/examples/plugins/say_export.rb +1 -4
  94. 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
@@ -166,11 +119,17 @@ module Doing
166
119
 
167
120
  output.gsub!(/\\%/, '%')
168
121
 
122
+ output.highlight_search!(opt[:search]) if opt[:search] && !opt[:not] && opt[:hilite]
123
+
169
124
  out += "#{output}\n"
170
125
  end
171
126
 
172
127
  # 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]
128
+ if opt[:totals]
129
+ out += wwid.tag_times(format: wwid.config['timer_format'].to_sym,
130
+ sort_by_name: opt[:sort_tags],
131
+ sort_order: opt[:tag_order])
132
+ end
174
133
  out
175
134
  end
176
135
 
@@ -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
@@ -17,12 +17,74 @@ module Doing
17
17
  end
18
18
 
19
19
  def enter_text(prompt, default_response: '')
20
+ $stdin.reopen('/dev/tty')
20
21
  return default_response if @default_answer
21
22
 
22
23
  print "#{yellow(prompt).sub(/:?$/, ':')} #{reset}"
23
24
  $stdin.gets.strip
24
25
  end
25
26
 
27
+ def read_line(prompt: 'Enter text', completions: [], default_response: '')
28
+ $stdin.reopen('/dev/tty')
29
+ return default_response if @default_answer
30
+
31
+ unless completions.empty?
32
+ completions.sort!
33
+ comp = proc { |s| completions.grep(/^#{Regexp.escape(s)}/) }
34
+ Readline.completion_append_character = ' '
35
+ Readline.completion_proc = comp
36
+ end
37
+
38
+ begin
39
+ Readline.readline("#{yellow(prompt).sub(/:?$/, ':')} #{reset}", true).strip
40
+ rescue Interrupt
41
+ raise UserCancelled
42
+ end
43
+ end
44
+
45
+ def read_lines(prompt: 'Enter text', completions: [])
46
+ $stdin.reopen('/dev/tty')
47
+ return default_response if @default_answer
48
+
49
+ completions.sort!
50
+ comp = proc { |s| completions.grep(/^#{Regexp.escape(s)}/) }
51
+ Readline.completion_append_character = ' '
52
+ Readline.completion_proc = comp
53
+ prompt_text = []
54
+ prompt_text << boldgreen(prompt.sub(/:?$/, ':'))
55
+ prompt_text << yellow(' Enter a blank line (')
56
+ prompt_text << boldwhite('return twice')
57
+ prompt_text << yellow(') to end editing')
58
+ puts prompt_text.join('')
59
+
60
+ res = []
61
+
62
+ begin
63
+ while (line = Readline.readline('> ', true))
64
+ break if line.strip.empty?
65
+
66
+ res << line.chomp
67
+ end
68
+ rescue Interrupt
69
+ raise UserCancelled
70
+ end
71
+
72
+ res.join("\n").strip
73
+ end
74
+
75
+ def request_lines(prompt: 'Enter text')
76
+ $stdin.reopen('/dev/tty')
77
+ ask_note = []
78
+ reader = TTY::Reader.new(interrupt: -> { raise Errors::UserCancelled }, track_history: false)
79
+ puts "#{boldgreen(prompt.sub(/:?$/, ':'))} #{yellow('Hit return for a new line, ')}#{boldwhite('enter a blank line (')}#{boldyellow('return twice')}#{boldwhite(') to end editing')}"
80
+ loop do
81
+ res = reader.read_line(green('> '))
82
+ break if res.strip.empty?
83
+
84
+ ask_note.push(res)
85
+ end
86
+ ask_note.join("\n").strip
87
+ end
26
88
 
27
89
  ##
28
90
  ## Ask a yes or no question in the terminal
@@ -39,6 +101,8 @@ module Doing
39
101
  return @force_answer
40
102
  end
41
103
 
104
+ $stdin.reopen('/dev/tty')
105
+
42
106
  default = if default_response.is_a?(String)
43
107
  default_response =~ /y/i ? true : false
44
108
  else
@@ -205,7 +269,7 @@ module Doing
205
269
  out = [
206
270
  format("%#{pad}d", i),
207
271
  ') ',
208
- format('%13s', item.date.relative_date),
272
+ format('%16s', item.date.strftime('%Y-%m-%d %H:%M')),
209
273
  ' | ',
210
274
  item.title
211
275
  ]
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