doing 2.1.13 → 2.1.17

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 (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