doing 2.1.4pre → 2.1.7

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 (109) hide show
  1. checksums.yaml +4 -4
  2. data/.yardoc/checksums +14 -13
  3. data/.yardoc/object_types +0 -0
  4. data/.yardoc/objects/root.dat +0 -0
  5. data/.yardopts +1 -1
  6. data/CHANGELOG.md +37 -1
  7. data/Gemfile.lock +3 -1
  8. data/README.md +5 -1
  9. data/bin/doing +192 -95
  10. data/docs/_config.yml +1 -0
  11. data/{doc → docs/doc}/Array.html +63 -1
  12. data/docs/doc/BooleanTermParser/Clause.html +293 -0
  13. data/docs/doc/BooleanTermParser/Operator.html +172 -0
  14. data/docs/doc/BooleanTermParser/Query.html +417 -0
  15. data/docs/doc/BooleanTermParser/QueryParser.html +135 -0
  16. data/docs/doc/BooleanTermParser/QueryTransformer.html +124 -0
  17. data/docs/doc/BooleanTermParser.html +115 -0
  18. data/docs/doc/Doing/CLIFormat.html +131 -0
  19. data/{doc → docs/doc}/Doing/Color.html +2 -2
  20. data/{doc → docs/doc}/Doing/Completion.html +1 -1
  21. data/{doc → docs/doc}/Doing/Configuration.html +117 -12
  22. data/{doc → docs/doc}/Doing/Content.html +0 -0
  23. data/{doc → docs/doc}/Doing/Errors/DoingNoTraceError.html +1 -1
  24. data/{doc → docs/doc}/Doing/Errors/DoingRuntimeError.html +1 -1
  25. data/{doc → docs/doc}/Doing/Errors/DoingStandardError.html +1 -1
  26. data/{doc → docs/doc}/Doing/Errors/EmptyInput.html +1 -1
  27. data/{doc → docs/doc}/Doing/Errors/NoResults.html +1 -1
  28. data/{doc → docs/doc}/Doing/Errors/PluginException.html +1 -1
  29. data/{doc → docs/doc}/Doing/Errors/UserCancelled.html +1 -1
  30. data/{doc → docs/doc}/Doing/Errors/WrongCommand.html +1 -1
  31. data/{doc → docs/doc}/Doing/Errors.html +1 -1
  32. data/{doc → docs/doc}/Doing/Hooks.html +1 -1
  33. data/{doc → docs/doc}/Doing/Item.html +100 -73
  34. data/{doc → docs/doc}/Doing/Items.html +2 -2
  35. data/{doc → docs/doc}/Doing/LogAdapter.html +70 -1
  36. data/{doc → docs/doc}/Doing/Note.html +5 -134
  37. data/{doc → docs/doc}/Doing/Pager.html +1 -1
  38. data/{doc → docs/doc}/Doing/Plugins.html +431 -35
  39. data/{doc → docs/doc}/Doing/Prompt.html +1 -1
  40. data/{doc → docs/doc}/Doing/Section.html +1 -1
  41. data/docs/doc/Doing/TemplateString.html +713 -0
  42. data/docs/doc/Doing/Util/Backup.html +686 -0
  43. data/{doc → docs/doc}/Doing/Util.html +1 -1
  44. data/{doc → docs/doc}/Doing/WWID.html +5 -5
  45. data/{doc → docs/doc}/Doing/WWIDFile.html +0 -0
  46. data/{doc → docs/doc}/Doing.html +4 -4
  47. data/{doc → docs/doc}/GLI/Commands/MarkdownDocumentListener.html +1 -1
  48. data/{doc → docs/doc}/GLI/Commands.html +1 -1
  49. data/{doc → docs/doc}/GLI.html +1 -1
  50. data/{doc → docs/doc}/Hash.html +1 -1
  51. data/docs/doc/PhraseParser/Operator.html +172 -0
  52. data/docs/doc/PhraseParser/PhraseClause.html +303 -0
  53. data/docs/doc/PhraseParser/Query.html +495 -0
  54. data/docs/doc/PhraseParser/QueryParser.html +136 -0
  55. data/docs/doc/PhraseParser/QueryTransformer.html +124 -0
  56. data/docs/doc/PhraseParser/TermClause.html +293 -0
  57. data/docs/doc/PhraseParser.html +115 -0
  58. data/{doc → docs/doc}/Status.html +1 -1
  59. data/{doc → docs/doc}/String.html +182 -12
  60. data/{doc → docs/doc}/Symbol.html +35 -1
  61. data/{doc → docs/doc}/Time.html +1 -1
  62. data/{doc → docs/doc}/_index.html +21 -14
  63. data/{doc → docs/doc}/class_list.html +1 -1
  64. data/{doc → docs/doc}/css/common.css +0 -0
  65. data/{doc → docs/doc}/css/full_list.css +0 -0
  66. data/{doc → docs/doc}/css/style.css +0 -0
  67. data/{doc → docs/doc}/file.README.html +6 -2
  68. data/{doc → docs/doc}/file_list.html +0 -0
  69. data/{doc → docs/doc}/frames.html +0 -0
  70. data/{doc → docs/doc}/index.html +6 -2
  71. data/{doc → docs/doc}/js/app.js +0 -0
  72. data/{doc → docs/doc}/js/full_list.js +0 -0
  73. data/{doc → docs/doc}/js/jquery.js +0 -0
  74. data/{doc → docs/doc}/method_list.html +313 -161
  75. data/{doc → docs/doc}/top-level-namespace.html +1 -1
  76. data/docs/index.md +60 -0
  77. data/doing.gemspec +1 -0
  78. data/doing.rdoc +74 -15
  79. data/example_plugin.rb +3 -1
  80. data/lib/completion/_doing.zsh +53 -41
  81. data/lib/completion/doing.bash +17 -6
  82. data/lib/completion/doing.fish +321 -2
  83. data/lib/doing/array.rb +9 -0
  84. data/lib/doing/completion/fish_completion.rb +46 -3
  85. data/lib/doing/completion/zsh_completion.rb +1 -1
  86. data/lib/doing/configuration.rb +33 -11
  87. data/lib/doing/item.rb +12 -3
  88. data/lib/doing/log_adapter.rb +28 -0
  89. data/lib/doing/note.rb +31 -30
  90. data/lib/doing/plugin_manager.rb +84 -21
  91. data/lib/doing/plugins/export/dayone_export.rb +209 -0
  92. data/lib/doing/plugins/export/html_export.rb +2 -2
  93. data/lib/doing/plugins/export/json_export.rb +1 -0
  94. data/lib/doing/plugins/export/markdown_export.rb +1 -1
  95. data/lib/doing/plugins/export/template_export.rb +90 -85
  96. data/lib/doing/prompt.rb +9 -6
  97. data/lib/doing/string.rb +68 -27
  98. data/lib/doing/symbol.rb +4 -0
  99. data/lib/doing/template_string.rb +197 -0
  100. data/lib/doing/util.rb +4 -2
  101. data/lib/doing/util_backup.rb +55 -3
  102. data/lib/doing/version.rb +1 -1
  103. data/lib/doing/wwid.rb +37 -22
  104. data/lib/doing.rb +3 -0
  105. data/lib/examples/plugins/say_export.rb +1 -1
  106. data/lib/examples/plugins/wiki_export/wiki_export.rb +3 -3
  107. data/lib/templates/doing-dayone-entry.erb +6 -0
  108. data/lib/templates/doing-dayone.erb +5 -0
  109. metadata +95 -53
@@ -23,13 +23,15 @@ module Doing
23
23
  out = ''
24
24
  items.each do |item|
25
25
  if opt[:highlight] && item.title =~ /@#{wwid.config['marker_tag']}\b/i
26
- # flag = Doing::Color.send(wwid.config['marker_color'])
27
- reset = Doing::Color.default
26
+ flag = Doing::Color.send(wwid.config['marker_color'])
27
+ reset = Doing::Color.reset + Doing::Color.default
28
28
  else
29
- # flag = ''
29
+ flag = ''
30
30
  reset = ''
31
31
  end
32
32
 
33
+ placeholders = {}
34
+
33
35
  if (!item.note.empty?) && wwid.config['include_notes']
34
36
  note = item.note.map(&:strip).delete_if(&:empty?)
35
37
  note.map! { |line| "#{line.sub(/^\t*/, '')} " }
@@ -40,26 +42,17 @@ module Doing
40
42
  line.simple_wrap(width)
41
43
  # line.chomp.gsub(/(.{1,#{width}})(\s+|\Z)/, "\\1\n")
42
44
  end
43
- note = note.join("\n").split(/\n/).delete_if(&:empty?)
45
+ note = note.delete_if(&:empty?)
44
46
  end
45
47
  else
46
48
  note = []
47
49
  end
48
50
 
49
- output = opt[:template].dup
50
-
51
- output.gsub!(/%[a-z]+/) do |m|
52
- if Doing::Color.respond_to?(m.sub(/^%/, ''))
53
- Doing::Color.send(m.sub(/^%/, ''))
54
- else
55
- m
56
- end
57
- end
58
-
59
- output.sub!(/%(\d+)?date/) do
60
- pad = Regexp.last_match(1).to_i
61
- format("%#{pad}s", item.date.strftime(opt[:format]))
62
- end
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
+ placeholders['date'] = item.date.strftime(opt[:format])
63
56
 
64
57
  interval = wwid.get_interval(item, record: true, formatted: false) if opt[:times]
65
58
  if interval
@@ -74,7 +67,8 @@ module Doing
74
67
  end
75
68
 
76
69
  interval ||= ''
77
- output.sub!(/%interval/, interval)
70
+ # output.sub!(/%interval/, interval)
71
+ placeholders['interval'] = interval
78
72
 
79
73
  duration = item.duration if opt[:duration]
80
74
  if duration
@@ -88,78 +82,89 @@ module Doing
88
82
  end
89
83
  end
90
84
  duration ||= ''
91
- output.sub!(/%duration/, duration)
92
-
93
- output.sub!(/%(\d+)?shortdate/) do
94
- pad = Regexp.last_match(1) || 13
95
- format("%#{pad}s", item.date.relative_date)
96
- end
97
-
98
- output.sub!(/%section/, item.section) if item.section
99
-
100
- title_rx = /(?mi)%(?<width>-?\d+)?(?:(?<ichar>[ _t])(?<icount>\d+))?(?<prefix>.[ _t]?)?title(?<after>.*?)$/
101
- title_color = Doing::Color.reset + output.match(/(?mi)^(.*?)(%.*?title)/)[1].last_color
102
-
103
- title_offset = Doing::Color.uncolor(output).match(title_rx).begin(0)
104
-
105
- output.sub!(title_rx) do
106
- m = Regexp.last_match
107
-
108
- after = m['after']
109
- pad = m['width'].to_i
110
- indent = ''
111
- if m['ichar']
112
- char = m['ichar'] =~ /t/ ? "\t" : ' '
113
- indent = char * m['icount'].to_i
114
- end
115
- prefix = m['prefix']
116
- if opt[:wrap_width]&.positive? || pad.positive?
117
- width = pad.positive? ? pad : opt[:wrap_width]
118
- item.title.wrap(width, pad: pad, indent: indent, offset: title_offset, prefix: prefix, color: title_color, after: after, reset: reset)
119
- # flag + item.title.gsub(/(.{#{opt[:wrap_width]}})(?=\s+|\Z)/, "\\1\n ").sub(/\s*$/, '') + reset
120
- else
121
- format("%s%#{pad}s%s", prefix, item.title.sub(/\s*$/, ''), after)
122
- end
123
- end
124
-
125
- # output.sub!(/(?i-m)^([\s\S]*?)(%(?:[io]d|(?:\^[\s\S])?(?:(?:[ _t]|[^a-z0-9])?\d+)?(?:[\s\S][ _t]?)?)?note)([\s\S]*?)$/, '\1\3\2')
126
- if opt[:tags_color]
127
- output.highlight_tags!(opt[:tags_color])
128
- end
129
-
130
- if note.empty?
131
- output.gsub!(/%(chomp|[io]d|(\^.)?(([ _t]|[^a-z0-9])?\d+)?(.[ _t]?)?)?note/, '')
132
- else
133
- output.sub!(/%note/, "\n#{note.map { |l| "\t#{l.strip} " }.join("\n")}")
134
- output.sub!(/%idnote/, "\n#{note.map { |l| "\t\t#{l.strip} " }.join("\n")}")
135
- output.sub!(/%odnote/, "\n#{note.map { |l| "#{l.strip} " }.join("\n")}")
136
- output.sub!(/(?mi)%(?:\^(?<mchar>.))?(?:(?<ichar>[ _t]|[^a-z0-9])?(?<icount>\d+))?(?<prefix>.[ _t]?)?note/) do
137
- m = Regexp.last_match
138
- mark = m['mchar'] || ''
139
- indent = if m['ichar']
140
- char = m['ichar'] =~ /t/ ? "\t" : ' '
141
- char * m['icount'].to_i
142
- else
143
- ''
144
- end
145
- prefix = m['prefix'] || ''
146
- "\n#{note.map { |l| "#{mark}#{indent}#{prefix}#{l.strip} " }.join("\n")}"
147
- end
148
-
149
- output.sub!(/%chompnote/) do
150
- note.map { |l| l.gsub(/\n+/, ' ').gsub(/(^\s*|\s*$)/, '').gsub(/\s+/, ' ') }.join(' ')
151
- end
152
- end
153
-
154
- output.gsub!(/%hr(_under)?/) do
85
+ # output.sub!(/%duration/, duration)
86
+ placeholders['duration'] = duration
87
+
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
94
+ placeholders['section'] = item.section || ''
95
+ 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
+ placeholders['note'] = note
125
+ placeholders['idnote'] = note.empty? ? '' : "\n#{note.map { |l| "\t\t#{l.strip} " }.join("\n")}"
126
+ 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
+
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
156
+
157
+ output.gsub!(/(?<!\\)%hr(_under)?/) do
155
158
  o = ''
156
159
  `tput cols`.to_i.times do
157
160
  o += Regexp.last_match(1).nil? ? '-' : '_'
158
161
  end
159
162
  o
160
163
  end
161
- output.gsub!(/%n/, "\n")
162
- output.gsub!(/%t/, "\t")
164
+ output.gsub!(/(?<!\\)%n/, "\n")
165
+ output.gsub!(/(?<!\\)%t/, "\t")
166
+
167
+ output.gsub!(/\\%/, '%')
163
168
 
164
169
  out += "#{output}\n"
165
170
  end
data/lib/doing/prompt.rb CHANGED
@@ -118,14 +118,17 @@ module Doing
118
118
  return nil unless $stdout.isatty
119
119
 
120
120
  # fzf_args << '-1' # User is expecting a menu, and even if only one it seves as confirmation
121
- fzf_args << %(--prompt="#{prompt}")
122
- fzf_args << "--height=#{options.count + 2}"
123
- fzf_args << '--info=inline'
124
- fzf_args << '--multi' if multiple
121
+ default_args = []
122
+ default_args << %(--prompt="#{prompt}")
123
+ default_args << "--height=#{options.count + 2}"
124
+ default_args << '--info=inline'
125
+ default_args << '--multi' if multiple
125
126
  header = "esc: cancel,#{multiple ? ' tab: multi-select, ctrl-a: select all,' : ''} return: confirm"
126
- fzf_args << %(--header="#{header}")
127
+ default_args << %(--header="#{header}")
128
+ default_args.concat(fzf_args)
127
129
  options.sort! if sorted
128
- res = `echo #{Shellwords.escape(options.join("\n"))}|#{fzf} #{fzf_args.join(' ')}`
130
+
131
+ res = `echo #{Shellwords.escape(options.join("\n"))}|#{fzf} #{default_args.join(' ')}`
129
132
  return false if res.strip.size.zero?
130
133
 
131
134
  res
data/lib/doing/string.rb CHANGED
@@ -28,7 +28,9 @@ module Doing
28
28
  ##
29
29
  ## @return [Regexp] Regex pattern
30
30
  ##
31
- def to_rx(distance: 3, case_type: :smart)
31
+ def to_rx(distance: nil, case_type: nil)
32
+ distance ||= Doing.config.settings.dig('search', 'distance').to_i || 3
33
+ case_type ||= Doing.config.settings.dig('search', 'case')&.normalize_case || :smart
32
34
  case_sensitive = case case_type
33
35
  when :smart
34
36
  self =~ /[A-Z]/ ? true : false
@@ -44,7 +46,9 @@ module Doing
44
46
  when /^'/
45
47
  sub(/^'(.*?)'?$/, '\1')
46
48
  else
47
- split(/ +/).map { |w| w.split('').join(".{0,#{distance}}") }.join('.*?')
49
+ split(/ +/).map do |w|
50
+ w.split('').join(".{0,#{distance}}").gsub(/\+/, '\+').wildcard_to_rx
51
+ end.join('.*?')
48
52
  end
49
53
  Regexp.new(pattern, !case_sensitive)
50
54
  end
@@ -72,7 +76,7 @@ module Doing
72
76
  end
73
77
 
74
78
  ## @param (see #highlight_tags)
75
- def highlight_tags!(color = 'yellow')
79
+ def highlight_tags!(color = 'yellow', last_color: nil)
76
80
  replace highlight_tags(color)
77
81
  end
78
82
 
@@ -83,17 +87,18 @@ module Doing
83
87
  ##
84
88
  ## @return [String] string with @tags highlighted
85
89
  ##
86
- def highlight_tags(color = 'yellow')
87
- escapes = scan(/(\e\[[\d;]+m)[^\e]+@/)
88
- color = color.split(' ') unless color.is_a?(Array)
89
- tag_color = ''
90
- color.each { |c| tag_color += Doing::Color.send(c) }
91
- last_color = if !escapes.empty?
92
- escapes[-1][0]
93
- else
94
- Doing::Color.default
95
- end
96
- gsub(/(\s|m)(@[^ ("']+)/, "\\1#{tag_color}\\2#{Doing::Color.reset}#{last_color}")
90
+ def highlight_tags(color = 'yellow', last_color: nil)
91
+ unless last_color
92
+ escapes = scan(/(\e\[[\d;]+m)[^\e]+@/)
93
+ color = color.split(' ') unless color.is_a?(Array)
94
+ tag_color = color.each_with_object([]) { |c, arr| arr << Doing::Color.send(c) }.join('')
95
+ last_color = if !escapes.empty?
96
+ (escapes.count > 1 ? escapes[-2..-1] : [escapes[-1]]).map { |v| v[0] }.join('')
97
+ else
98
+ Doing::Color.default
99
+ end
100
+ end
101
+ gsub(/(\s|m)(@[^ ("']+)/, "\\1#{tag_color}\\2#{last_color}")
97
102
  end
98
103
 
99
104
  ##
@@ -192,11 +197,25 @@ module Doing
192
197
  ## @param offset [Integer] (Optional) The width to pad each subsequent line
193
198
  ## @param prefix [String] (Optional) A prefix to add to each line
194
199
  ##
195
- def wrap(len, pad: 0, indent: ' ', offset: 0, prefix: '', color: '', after: '', reset: '')
200
+ def wrap(len, pad: 0, indent: ' ', offset: 0, prefix: '', color: '', after: '', reset: '', pad_first: false)
196
201
  last_color = color.empty? ? '' : after.last_color
197
- note_rx = /(?i-m)(%(?:[io]d|(?:\^[\s\S])?(?:(?:[ _t]|[^a-z0-9])?\d+)?(?:[\s\S][ _t]?)?)?note)/
202
+ note_rx = /(?mi)(?<!\\)%(?<width>-?\d+)?(?:\^(?<mchar>.))?(?:(?<ichar>[ _t]|[^a-z0-9])(?<icount>\d+))?(?<prefix>.[ _t]?)?note/
203
+ note = ''
204
+ after = after.dup if after.frozen?
205
+ after.sub!(note_rx) do
206
+ note = Regexp.last_match(0)
207
+ ''
208
+ end
209
+
210
+ left_pad = ' ' * offset
211
+ left_pad += indent
212
+
213
+
214
+ # return "#{left_pad}#{prefix}#{color}#{self}#{last_color} #{note}" unless len.positive?
215
+
198
216
  # Don't break inside of tag values
199
- str = gsub(/@\S+\(.*?\)/) { |tag| tag.gsub(/\s/, '%%%%') }
217
+ str = gsub(/@\S+\(.*?\)/) { |tag| tag.gsub(/\s/, '%%%%') }.gsub(/\n/, ' ')
218
+
200
219
  words = str.split(/ /).map { |word| word.gsub(/%%%%/, ' ') }
201
220
  out = []
202
221
  line = []
@@ -215,18 +234,18 @@ module Doing
215
234
  line << word.uncolor
216
235
  end
217
236
  out.push(line.join(' '))
218
- note = ''
219
- after = after.dup if after.frozen?
220
- after.sub!(note_rx) do
221
- note = Regexp.last_match(0)
222
- ''
223
- end
224
237
 
238
+ last_color = ''
225
239
  out[0] = format("%-#{pad}s%s%s", out[0], last_color, after)
226
240
 
227
- left_pad = ' ' * offset
228
- left_pad += indent
229
- out.map { |l| "#{left_pad}#{color}#{l}#{last_color}" }.join("\n").strip + last_color + " #{note}".chomp
241
+ out.map.with_index { |l, idx|
242
+ if !pad_first && idx == 0
243
+ "#{color}#{prefix}#{l}#{last_color}"
244
+ else
245
+ "#{left_pad}#{color}#{prefix}#{l}#{last_color}"
246
+ end
247
+ }.join("\n") + " #{note}".chomp
248
+ # res.join("\n").strip + last_color + " #{note}".chomp
230
249
  end
231
250
 
232
251
  ##
@@ -275,7 +294,7 @@ module Doing
275
294
 
276
295
  def normalize_case(default = :smart)
277
296
  case self
278
- when /^c/i
297
+ when /^(c|sens)/i
279
298
  :sensitive
280
299
  when /^i/i
281
300
  :ignore
@@ -310,6 +329,28 @@ module Doing
310
329
  end
311
330
  end
312
331
 
332
+ ##
333
+ ## Convert a matching configuration string to a symbol
334
+ ##
335
+ ## @return Symbol :fuzzy, :pattern, :exact
336
+ ##
337
+ def normalize_matching!(default = :pattern)
338
+ replace normalize_bool(default)
339
+ end
340
+
341
+ def normalize_matching(default = :pattern)
342
+ case self
343
+ when /^f/i
344
+ :fuzzy
345
+ when /^p/i
346
+ :pattern
347
+ when /^e/i
348
+ :exact
349
+ else
350
+ default.is_a?(Symbol) ? default : default.normalize_matching
351
+ end
352
+ end
353
+
313
354
  def normalize_trigger!
314
355
  replace normalize_trigger
315
356
  end
data/lib/doing/symbol.rb CHANGED
@@ -16,5 +16,9 @@ module Doing
16
16
  def normalize_case
17
17
  self
18
18
  end
19
+
20
+ def normalize_matching(default = :pattern)
21
+ to_s.normalize_matching(default)
22
+ end
19
23
  end
20
24
  end
@@ -0,0 +1,197 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doing
4
+ ##
5
+ ## Template string formatting
6
+ ##
7
+ class TemplateString < String
8
+ class ::String
9
+ ##
10
+ ## Extract the longest valid color from a string.
11
+ ##
12
+ ## Allows %colors to bleed into other text and still
13
+ ## be recognized, e.g. %greensomething still finds
14
+ ## %green.
15
+ ##
16
+ ## @return [String] a valid color name
17
+ ## @api private
18
+ def validate_color
19
+ valid_color = nil
20
+ compiled = ''
21
+ split('').each do |char|
22
+ compiled += char
23
+ valid_color = compiled if Color.attributes.include?(compiled.to_sym)
24
+ end
25
+
26
+ valid_color
27
+ end
28
+ end
29
+
30
+ attr_reader :original
31
+
32
+ include Color
33
+ def initialize(string, placeholders: {}, force_color: false, wrap_width: 0, color: '', tags_color: '', reset: '')
34
+ Color.coloring = true if force_color
35
+ @colors = nil
36
+ @original = string
37
+ super(Color.reset + string)
38
+
39
+ placeholders.each { |k, v| fill(k, v, wrap_width: wrap_width, color: color, tags_color: tags_color) }
40
+ end
41
+
42
+ ##
43
+ ## Test if string contains any valid %colors
44
+ ##
45
+ ## @return [Boolean] True if colors, False otherwise.
46
+ ##
47
+ def colors?
48
+ scan(/%([a-z]+)/).each do
49
+ return true if Regexp.last_match(1).validate_color
50
+ end
51
+ false
52
+ end
53
+
54
+ def reparse
55
+ @parsed_colors = nil
56
+ end
57
+
58
+ ##
59
+ ## Return string with %colors replaced with escape codes
60
+ ##
61
+ ## @return [String] colorized string
62
+ ##
63
+ def colored
64
+ reparse
65
+ parsed_colors[:string].apply_colors(parsed_colors[:colors])
66
+ end
67
+
68
+ ##
69
+ ## Remove all valid %colors from string
70
+ ##
71
+ ## @return [String] cleaned string
72
+ ##
73
+ def raw
74
+ parsed_colors[:string].uncolor
75
+ end
76
+
77
+ def parsed_colors
78
+ @parsed_colors ||= parse_colors
79
+ end
80
+
81
+ ##
82
+ ## Parse a template string for %colors and return a hash
83
+ ## of colors and string locations
84
+ ##
85
+ ## @return [Hash] Uncolored string and array of colors and locations
86
+ def parse_colors
87
+ working = dup
88
+ color_array = []
89
+
90
+ scan(/(?<!\\)(%([a-z]+))/).each do |color|
91
+ valid_color = color[1].validate_color
92
+ next unless valid_color
93
+
94
+ idx = working.match(/(?<!\\)%#{valid_color}/).begin(0)
95
+ color_array.push({ name: valid_color, color: Color.send(valid_color), index: idx })
96
+ working.sub!(/(?<!\\)%#{valid_color}/, '')
97
+ end
98
+
99
+ { string: working, colors: color_array }
100
+ end
101
+
102
+ ##
103
+ ## Apply a color array to a string
104
+ ##
105
+ ## @param color_array [Array] Array of hashes
106
+ ## containing :name, :color,
107
+ ## :index
108
+ ##
109
+ def apply_colors(color_array)
110
+ str = dup
111
+ color_array.reverse.each do |color|
112
+ c = color[:color].empty? ? Color.send(color[:name]) : color[:color]
113
+ str.insert(color[:index], c)
114
+ end
115
+ str
116
+ end
117
+
118
+ def fill(placeholder, value, wrap_width: 0, color: '', tags_color: '', reset: '')
119
+ reparse
120
+ rx = /(?mi)(?<!\\)%(?<width>-?\d+)?(?:\^(?<mchar>.))?(?:(?<ichar>[ _t]|[^a-z0-9])(?<icount>\d+))?(?<prefix>.[ _t]?)?#{placeholder.sub(/^%/, '')}(?<after>.*?)$/
121
+ ph = raw.match(rx)
122
+
123
+ return unless ph
124
+ placeholder_offset = ph.begin(0)
125
+ last_colors = parsed_colors[:colors].select { |v| v[:index] <= placeholder_offset + 4 }
126
+
127
+ last_color = last_colors.map { |v| v[:color] }.pop(3).join('')
128
+
129
+ sub!(rx) do
130
+ m = Regexp.last_match
131
+
132
+ after = m['after']
133
+
134
+ if value.nil? || value.empty?
135
+ after
136
+ else
137
+ pad = m['width'].to_i
138
+ mark = m['mchar'] || ''
139
+ if placeholder == 'shortdate' && m['width'].nil?
140
+ pad = 13
141
+ end
142
+ indent = nil
143
+ if m['ichar']
144
+ char = m['ichar'] =~ /t/ ? "\t" : ' '
145
+ indent = char * m['icount'].to_i
146
+ end
147
+ indent ||= placeholder =~ /^title/ ? '' : "\t"
148
+ prefix = m['prefix']
149
+ if placeholder =~ /^title/
150
+ color = last_color + color
151
+
152
+ if wrap_width.positive? || pad.positive?
153
+ width = pad.positive? ? pad : wrap_width
154
+
155
+ out = value.gsub(/%/, '\%').strip.wrap(width,
156
+ pad: pad,
157
+ indent: indent,
158
+ offset: placeholder_offset,
159
+ prefix: prefix,
160
+ color: color,
161
+ after: after,
162
+ reset: reset,
163
+ pad_first: false)
164
+ out.highlight_tags!(tags_color, last_color: color) if tags_color && !tags_color.empty?
165
+ out
166
+ else
167
+ out = format("%s%s%#{pad}s%s", prefix, color, value.gsub(/%/, '\%').sub(/\s*$/, ''), after)
168
+ out.highlight_tags!(tags_color, last_color: color) if tags_color && !tags_color.empty?
169
+ out
170
+ end
171
+ elsif placeholder =~ /^note/
172
+ if wrap_width.positive? || pad.positive?
173
+ width = pad.positive? ? pad : wrap_width
174
+ outstring = value.map do |l|
175
+ if l.empty?
176
+ ' '
177
+ else
178
+ line = l.gsub(/%/, '\%').strip.wrap(width, pad: pad, indent: indent, offset: 0, prefix: prefix, color: last_color, after: after, reset: reset, pad_first: true)
179
+ line.highlight_tags!(tags_color, last_color: last_color) unless tags_color.nil? || tags_color.empty?
180
+ "#{line} "
181
+ end
182
+ end.join("\n")
183
+ "\n#{last_color}#{mark}#{outstring} "
184
+ else
185
+ out = format("\n%s%s%s%#{pad}s%s", indent, prefix, last_color, value.join("\n#{indent}#{prefix}").gsub(/%/, '\%').sub(/\s*$/, ''), after)
186
+ out.highlight_tags!(tags_color, last_color: last_color) if tags_color && !tags_color.empty?
187
+ out
188
+ end
189
+ else
190
+ format("%s%#{pad}s%s", prefix, value.gsub(/%/, '\%').sub(/\s*$/, ''), after)
191
+ end
192
+ end
193
+ end
194
+ @parsed_colors = parse_colors
195
+ end
196
+ end
197
+ end
data/lib/doing/util.rb CHANGED
@@ -112,7 +112,7 @@ module Doing
112
112
  puts content
113
113
  return
114
114
  end
115
-
115
+ Doing.logger.benchmark(:write_file, :start)
116
116
  file = File.expand_path(file)
117
117
 
118
118
  Backup.write_backup(file) if backup
@@ -121,8 +121,10 @@ module Doing
121
121
  f.puts content
122
122
  Doing.logger.debug('Write:', "File written: #{file}")
123
123
  end
124
-
124
+ Doing.logger.benchmark(:_post_write_hook, :start)
125
125
  Hooks.trigger :post_write, file
126
+ Doing.logger.benchmark(:_post_write_hook, :finish)
127
+ Doing.logger.benchmark(:write_file, :finish)
126
128
  end
127
129
 
128
130
  def safe_load_file(filename)