doing 2.1.24 → 2.1.28
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.
- checksums.yaml +4 -4
- data/.yardoc/checksums +17 -21
- data/.yardoc/object_types +0 -0
- data/.yardoc/objects/root.dat +0 -0
- data/CHANGELOG.md +325 -102
- data/Dockerfile +5 -5
- data/Dockerfile-2.6 +5 -5
- data/Dockerfile-2.7 +5 -4
- data/Dockerfile-3.0 +5 -4
- data/Gemfile.lock +1 -1
- data/README.md +1 -1
- data/Rakefile +3 -3
- data/bin/commands/add_section.rb +15 -0
- data/bin/commands/again.rb +57 -0
- data/bin/commands/archive.rb +55 -0
- data/bin/commands/cancel.rb +60 -0
- data/bin/commands/changes.rb +73 -0
- data/bin/commands/choose.rb +9 -0
- data/bin/commands/colors.rb +21 -0
- data/bin/commands/commands.rb +89 -0
- data/bin/commands/commands_accepting.rb +76 -0
- data/bin/commands/completion.rb +27 -0
- data/bin/commands/config.rb +245 -0
- data/bin/commands/done.rb +235 -0
- data/bin/commands/finish.rb +126 -0
- data/bin/commands/flag.rb +90 -0
- data/bin/commands/grep.rb +108 -0
- data/bin/commands/import.rb +71 -0
- data/bin/commands/install_fzf.rb +17 -0
- data/bin/commands/last.rb +81 -0
- data/bin/commands/meanwhile.rb +76 -0
- data/bin/commands/note.rb +91 -0
- data/bin/commands/now.rb +145 -0
- data/bin/commands/on.rb +65 -0
- data/bin/commands/open.rb +53 -0
- data/bin/commands/plugins.rb +23 -0
- data/bin/commands/recent.rb +77 -0
- data/bin/commands/redo.rb +26 -0
- data/bin/commands/reset.rb +73 -0
- data/bin/commands/rotate.rb +42 -0
- data/bin/commands/sections.rb +11 -0
- data/bin/commands/select.rb +105 -0
- data/bin/commands/show.rb +185 -0
- data/bin/commands/since.rb +63 -0
- data/bin/commands/tag.rb +149 -0
- data/bin/commands/tag_dir.rb +29 -0
- data/bin/commands/tags.rb +66 -0
- data/bin/commands/template.rb +61 -0
- data/bin/commands/today.rb +64 -0
- data/bin/commands/undo.rb +49 -0
- data/bin/commands/view.rb +201 -0
- data/bin/commands/views.rb +11 -0
- data/bin/commands/yesterday.rb +72 -0
- data/bin/doing +241 -3662
- data/docs/doc/Array.html +13 -449
- data/docs/doc/BooleanTermParser/Clause.html +5 -5
- data/docs/doc/BooleanTermParser/Operator.html +4 -4
- data/docs/doc/BooleanTermParser/Query.html +8 -8
- data/docs/doc/BooleanTermParser/QueryParser.html +2 -2
- data/docs/doc/BooleanTermParser/QueryTransformer.html +2 -2
- data/docs/doc/BooleanTermParser.html +1 -1
- data/docs/doc/Doing/Color.html +65 -59
- data/docs/doc/Doing/Completion.html +2 -2
- data/docs/doc/Doing/Configuration.html +49 -16
- data/docs/doc/Doing/Errors/DoingNoTraceError.html +2 -2
- data/docs/doc/Doing/Errors/DoingRuntimeError.html +2 -2
- data/docs/doc/Doing/Errors/DoingStandardError.html +2 -2
- data/docs/doc/Doing/Errors/EmptyInput.html +2 -2
- data/docs/doc/Doing/Errors/NoResults.html +2 -2
- data/docs/doc/Doing/Errors/PluginException.html +3 -3
- data/docs/doc/Doing/Errors/UserCancelled.html +2 -2
- data/docs/doc/Doing/Errors/WrongCommand.html +2 -2
- data/docs/doc/Doing/Errors.html +1 -1
- data/docs/doc/Doing/Hooks.html +6 -6
- data/docs/doc/Doing/Item.html +50 -16
- data/docs/doc/Doing/Items.html +10 -10
- data/docs/doc/Doing/LogAdapter.html +24 -24
- data/docs/doc/Doing/Note.html +7 -7
- data/docs/doc/Doing/Pager.html +4 -4
- data/docs/doc/Doing/Plugins.html +7 -7
- data/docs/doc/Doing/Prompt.html +59 -14
- data/docs/doc/Doing/Section.html +6 -6
- data/docs/doc/Doing/TemplateString.html +8 -8
- data/docs/doc/Doing/Types.html +46 -1
- data/docs/doc/Doing/Util/Backup.html +10 -10
- data/docs/doc/Doing/Util.html +15 -15
- data/docs/doc/Doing/WWID.html +73 -61
- data/docs/doc/Doing.html +3 -3
- data/docs/doc/FalseClass.html +235 -0
- data/docs/doc/GLI/Commands/Help.html +3 -3
- data/docs/doc/GLI/Commands/MarkdownDocumentListener.html +17 -17
- data/docs/doc/GLI/Commands.html +1 -1
- data/docs/doc/GLI.html +1 -1
- data/docs/doc/Hash.html +45 -11
- data/docs/doc/Numeric.html +5 -5
- data/docs/doc/Object.html +203 -0
- data/docs/doc/PhraseParser/Operator.html +4 -4
- data/docs/doc/PhraseParser/PhraseClause.html +5 -5
- data/docs/doc/PhraseParser/Query.html +10 -10
- data/docs/doc/PhraseParser/QueryParser.html +2 -2
- data/docs/doc/PhraseParser/QueryTransformer.html +2 -2
- data/docs/doc/PhraseParser/TermClause.html +5 -5
- data/docs/doc/PhraseParser.html +1 -1
- data/docs/doc/Status.html +7 -7
- data/docs/doc/String.html +306 -3111
- data/docs/doc/Symbol.html +45 -11
- data/docs/doc/Time.html +6 -6
- data/docs/doc/TrueClass.html +235 -0
- data/docs/doc/_index.html +37 -19
- data/docs/doc/class_list.html +1 -1
- data/docs/doc/file.README.html +2 -2
- data/docs/doc/index.html +2 -2
- data/docs/doc/method_list.html +240 -576
- data/docs/doc/top-level-namespace.html +2 -2
- data/doing.rdoc +297 -169
- data/example_plugin.rb +2 -2
- data/lib/completion/_doing.zsh +35 -31
- data/lib/completion/doing.bash +30 -19
- data/lib/completion/doing.fish +81 -67
- data/lib/doing/array/array.rb +4 -0
- data/lib/doing/array/nested_hash.rb +17 -0
- data/lib/doing/{array.rb → array/tags.rb} +7 -25
- data/lib/doing/changelog/change.rb +26 -11
- data/lib/doing/changelog/changes.rb +16 -4
- data/lib/doing/{array_chronify.rb → chronify/array.rb} +0 -0
- data/lib/doing/chronify/chronify.rb +5 -0
- data/lib/doing/{numeric_chronify.rb → chronify/numeric.rb} +0 -0
- data/lib/doing/{string_chronify.rb → chronify/string.rb} +0 -0
- data/lib/doing/colors.rb +115 -54
- data/lib/doing/configuration.rb +9 -6
- data/lib/doing/good.rb +72 -0
- data/lib/doing/hash.rb +4 -0
- data/lib/doing/help_monkey_patch.rb +6 -5
- data/lib/doing/hooks.rb +3 -3
- data/lib/doing/item.rb +19 -15
- data/lib/doing/items.rb +2 -2
- data/lib/doing/log_adapter.rb +35 -2
- data/lib/doing/normalize.rb +188 -0
- data/lib/doing/pager.rb +1 -0
- data/lib/doing/plugins/export/dayone_export.rb +1 -1
- data/lib/doing/plugins/export/html_export.rb +1 -1
- data/lib/doing/plugins/export/json_export.rb +1 -1
- data/lib/doing/plugins/export/markdown_export.rb +1 -1
- data/lib/doing/plugins/export/template_export.rb +3 -1
- data/lib/doing/plugins/import/calendar_import.rb +1 -1
- data/lib/doing/plugins/import/doing_import.rb +1 -1
- data/lib/doing/plugins/import/timing_import.rb +1 -1
- data/lib/doing/prompt.rb +9 -3
- data/lib/doing/string/highlight.rb +95 -0
- data/lib/doing/string/query.rb +129 -0
- data/lib/doing/string/string.rb +12 -0
- data/lib/doing/string/tags.rb +164 -0
- data/lib/doing/string/transform.rb +168 -0
- data/lib/doing/string/truncate.rb +75 -0
- data/lib/doing/string/url.rb +82 -0
- data/lib/doing/template_string.rb +2 -24
- data/lib/doing/types.rb +9 -0
- data/lib/doing/util.rb +20 -16
- data/lib/doing/version.rb +1 -1
- data/lib/doing/wwid.rb +91 -51
- data/lib/doing.rb +5 -6
- data/lib/examples/commands/wiki.rb +6 -7
- data/lib/examples/plugins/wiki_export/wiki_export.rb +1 -1
- data/lib/helpers/threaded_tests.rb +69 -79
- data/lib/helpers/threaded_tests_string.rb +50 -0
- data/scripts/deploy.rb +107 -0
- data/scripts/runtests.sh +4 -0
- metadata +65 -8
- data/lib/doing/string.rb +0 -765
- data/lib/doing/symbol.rb +0 -28
data/lib/doing/string.rb
DELETED
|
@@ -1,765 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Doing
|
|
4
|
-
##
|
|
5
|
-
## String helpers
|
|
6
|
-
##
|
|
7
|
-
class ::String
|
|
8
|
-
include Doing::Color
|
|
9
|
-
##
|
|
10
|
-
## Determines if receiver is surrounded by slashes or starts with single quote
|
|
11
|
-
##
|
|
12
|
-
## @return True if regex, False otherwise.
|
|
13
|
-
##
|
|
14
|
-
def is_rx?
|
|
15
|
-
self =~ %r{(^/.*?/$|^')}
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
##
|
|
19
|
-
## Convert string to fuzzy regex. Characters in words
|
|
20
|
-
## can be separated by up to *distance* characters in
|
|
21
|
-
## haystack, spaces indicate unlimited distance.
|
|
22
|
-
##
|
|
23
|
-
## @example
|
|
24
|
-
## "this word".to_rx(3)
|
|
25
|
-
## # => /t.{0,3}h.{0,3}i.{0,3}s.{0,3}.*?w.{0,3}o.{0,3}r.{0,3}d/
|
|
26
|
-
##
|
|
27
|
-
## @param distance [Integer] Allowed distance
|
|
28
|
-
## between characters
|
|
29
|
-
## @param case_type The case type
|
|
30
|
-
##
|
|
31
|
-
## @return [Regexp] Regex pattern
|
|
32
|
-
##
|
|
33
|
-
def to_rx(distance: nil, case_type: nil)
|
|
34
|
-
distance ||= Doing.config.settings.dig('search', 'distance').to_i || 3
|
|
35
|
-
case_type ||= Doing.config.settings.dig('search', 'case')&.normalize_case || :smart
|
|
36
|
-
case_sensitive = case case_type
|
|
37
|
-
when :smart
|
|
38
|
-
self =~ /[A-Z]/ ? true : false
|
|
39
|
-
when :sensitive
|
|
40
|
-
true
|
|
41
|
-
else
|
|
42
|
-
false
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
pattern = case dup.strip
|
|
46
|
-
when %r{^/.*?/$}
|
|
47
|
-
sub(%r{/(.*?)/}, '\1')
|
|
48
|
-
when /^'/
|
|
49
|
-
sub(/^'(.*?)'?$/, '\1')
|
|
50
|
-
else
|
|
51
|
-
split(/ +/).map do |w|
|
|
52
|
-
w.split('').join(".{0,#{distance}}").gsub(/\+/, '\+').wildcard_to_rx
|
|
53
|
-
end.join('.*?')
|
|
54
|
-
end
|
|
55
|
-
Regexp.new(pattern, !case_sensitive)
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
def to_phrase_query
|
|
59
|
-
parser = PhraseParser::QueryParser.new
|
|
60
|
-
transformer = PhraseParser::QueryTransformer.new
|
|
61
|
-
parse_tree = parser.parse(self)
|
|
62
|
-
transformer.apply(parse_tree).to_elasticsearch
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
def to_query
|
|
66
|
-
parser = BooleanTermParser::QueryParser.new
|
|
67
|
-
transformer = BooleanTermParser::QueryTransformer.new
|
|
68
|
-
parse_tree = parser.parse(self)
|
|
69
|
-
transformer.apply(parse_tree).to_elasticsearch
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
##
|
|
73
|
-
## Test string for truthiness (0, "f", "false", "n", "no" all return false, case insensitive, otherwise true)
|
|
74
|
-
##
|
|
75
|
-
## @return [Boolean] String is truthy
|
|
76
|
-
##
|
|
77
|
-
def truthy?
|
|
78
|
-
if self =~ /^(0|f(alse)?|n(o)?)$/i
|
|
79
|
-
false
|
|
80
|
-
else
|
|
81
|
-
true
|
|
82
|
-
end
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
# Compress multiple spaces to single space
|
|
86
|
-
def compress
|
|
87
|
-
gsub(/ +/, ' ').strip
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
def compress!
|
|
91
|
-
replace compress
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
## @param (see #highlight_tags)
|
|
95
|
-
def highlight_tags!(color = 'yellow', last_color: nil)
|
|
96
|
-
replace highlight_tags(color)
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
##
|
|
100
|
-
## Colorize @tags with ANSI escapes
|
|
101
|
-
##
|
|
102
|
-
## @param color [String] color (see #Color)
|
|
103
|
-
##
|
|
104
|
-
## @return [String] string with @tags highlighted
|
|
105
|
-
##
|
|
106
|
-
def highlight_tags(color = 'yellow', last_color: nil)
|
|
107
|
-
unless last_color
|
|
108
|
-
escapes = scan(/(\e\[[\d;]+m)[^\e]+@/)
|
|
109
|
-
color = color.split(' ') unless color.is_a?(Array)
|
|
110
|
-
tag_color = color.each_with_object([]) { |c, arr| arr << Doing::Color.send(c) }.join('')
|
|
111
|
-
last_color = if !escapes.empty?
|
|
112
|
-
(escapes.count > 1 ? escapes[-2..-1] : [escapes[-1]]).map { |v| v[0] }.join('')
|
|
113
|
-
else
|
|
114
|
-
Doing::Color.default
|
|
115
|
-
end
|
|
116
|
-
end
|
|
117
|
-
gsub(/(\s|m)(@[^ ("']+)/, "\\1#{tag_color}\\2#{last_color}")
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
def ignore_case(search, case_type)
|
|
121
|
-
(case_type == :smart && search !~ /[A-Z]/) || case_type == :ignore
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
def highlight_search!(search, distance: nil, negate: false, case_type: nil)
|
|
125
|
-
replace highlight_search(search, distance: distance, negate: negate, case_type: case_type)
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
def highlight_search(search, distance: nil, negate: false, case_type: nil)
|
|
129
|
-
out = dup
|
|
130
|
-
prefs = Doing.config.settings['search'] || {}
|
|
131
|
-
matching = prefs.fetch('matching', 'pattern').normalize_matching
|
|
132
|
-
distance ||= prefs.fetch('distance', 3).to_i
|
|
133
|
-
case_type ||= prefs.fetch('case', 'smart').normalize_case
|
|
134
|
-
|
|
135
|
-
if search.is_rx? || matching == :fuzzy
|
|
136
|
-
rx = search.to_rx(distance: distance, case_type: case_type)
|
|
137
|
-
out.gsub!(rx) { |m| m.bgyellow.black }
|
|
138
|
-
else
|
|
139
|
-
query = search.strip.to_phrase_query
|
|
140
|
-
|
|
141
|
-
if query[:must].nil? && query[:must_not].nil?
|
|
142
|
-
query[:must] = query[:should]
|
|
143
|
-
query[:should] = []
|
|
144
|
-
end
|
|
145
|
-
qs = []
|
|
146
|
-
qs.concat(query[:must]) if query[:must]
|
|
147
|
-
qs.concat(query[:should]) if query[:should]
|
|
148
|
-
qs.each do |s|
|
|
149
|
-
rx = Regexp.new(s.wildcard_to_rx, ignore_case(s, case_type))
|
|
150
|
-
out.gsub!(rx) { |m| m.bgyellow.black }
|
|
151
|
-
end
|
|
152
|
-
end
|
|
153
|
-
out
|
|
154
|
-
end
|
|
155
|
-
|
|
156
|
-
##
|
|
157
|
-
## Test if line should be ignored
|
|
158
|
-
##
|
|
159
|
-
## @return [Boolean] line is empty or comment
|
|
160
|
-
##
|
|
161
|
-
def ignore?
|
|
162
|
-
line = self
|
|
163
|
-
line =~ /^#/ || line =~ /^\s*$/
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
##
|
|
167
|
-
## Truncate to nearest word
|
|
168
|
-
##
|
|
169
|
-
## @param len The length
|
|
170
|
-
##
|
|
171
|
-
def truncate(len, ellipsis: '...')
|
|
172
|
-
return self if length <= len
|
|
173
|
-
|
|
174
|
-
total = 0
|
|
175
|
-
res = []
|
|
176
|
-
|
|
177
|
-
split(/ /).each do |word|
|
|
178
|
-
break if total + 1 + word.length > len
|
|
179
|
-
|
|
180
|
-
total += 1 + word.length
|
|
181
|
-
res.push(word)
|
|
182
|
-
end
|
|
183
|
-
res.join(' ') + ellipsis
|
|
184
|
-
end
|
|
185
|
-
|
|
186
|
-
def truncate!(len, ellipsis: '...')
|
|
187
|
-
replace truncate(len, ellipsis: ellipsis)
|
|
188
|
-
end
|
|
189
|
-
|
|
190
|
-
##
|
|
191
|
-
## Truncate string in the middle
|
|
192
|
-
##
|
|
193
|
-
## @param len The length
|
|
194
|
-
## @param ellipsis The ellipsis
|
|
195
|
-
##
|
|
196
|
-
def truncmiddle(len, ellipsis: '...')
|
|
197
|
-
return self if length <= len
|
|
198
|
-
len -= (ellipsis.length / 2).to_i
|
|
199
|
-
total = length
|
|
200
|
-
half = total / 2
|
|
201
|
-
cut = (total - len) / 2
|
|
202
|
-
sub(/(.{#{half - cut}}).*?(.{#{half - cut}})$/, "\\1#{ellipsis}\\2")
|
|
203
|
-
end
|
|
204
|
-
|
|
205
|
-
def truncmiddle!(len, ellipsis: '...')
|
|
206
|
-
replace truncmiddle(len, ellipsis: ellipsis)
|
|
207
|
-
end
|
|
208
|
-
|
|
209
|
-
##
|
|
210
|
-
## Remove color escape codes
|
|
211
|
-
##
|
|
212
|
-
## @return clean string
|
|
213
|
-
##
|
|
214
|
-
def uncolor
|
|
215
|
-
gsub(/\e\[[\d;]+m/,'')
|
|
216
|
-
end
|
|
217
|
-
|
|
218
|
-
def uncolor!
|
|
219
|
-
replace uncolor
|
|
220
|
-
end
|
|
221
|
-
|
|
222
|
-
def simple_wrap(width)
|
|
223
|
-
str = gsub(/@\S+\(.*?\)/) { |tag| tag.gsub(/\s/, '%%%%') }
|
|
224
|
-
words = str.split(/ /).map { |word| word.gsub(/%%%%/, ' ') }
|
|
225
|
-
out = []
|
|
226
|
-
line = []
|
|
227
|
-
|
|
228
|
-
words.each do |word|
|
|
229
|
-
if word.uncolor.length >= width
|
|
230
|
-
chars = word.uncolor.split('')
|
|
231
|
-
out << chars.slice!(0, width - 1).join('') while chars.count >= width
|
|
232
|
-
line << chars.join('')
|
|
233
|
-
next
|
|
234
|
-
elsif line.join(' ').uncolor.length + word.uncolor.length + 1 > width
|
|
235
|
-
out.push(line.join(' '))
|
|
236
|
-
line.clear
|
|
237
|
-
end
|
|
238
|
-
|
|
239
|
-
line << word.uncolor
|
|
240
|
-
end
|
|
241
|
-
out.push(line.join(' '))
|
|
242
|
-
out.join("\n")
|
|
243
|
-
end
|
|
244
|
-
|
|
245
|
-
##
|
|
246
|
-
## Wrap string at word breaks, respecting tags
|
|
247
|
-
##
|
|
248
|
-
## @param len [Integer] The length
|
|
249
|
-
## @param offset [Integer] (Optional) The width to pad each subsequent line
|
|
250
|
-
## @param prefix [String] (Optional) A prefix to add to each line
|
|
251
|
-
##
|
|
252
|
-
def wrap(len, pad: 0, indent: ' ', offset: 0, prefix: '', color: '', after: '', reset: '', pad_first: false)
|
|
253
|
-
last_color = color.empty? ? '' : after.last_color
|
|
254
|
-
note_rx = /(?mi)(?<!\\)%(?<width>-?\d+)?(?:\^(?<mchar>.))?(?:(?<ichar>[ _t]|[^a-z0-9])(?<icount>\d+))?(?<prefix>.[ _t]?)?note/
|
|
255
|
-
note = ''
|
|
256
|
-
after = after.dup if after.frozen?
|
|
257
|
-
after.sub!(note_rx) do
|
|
258
|
-
note = Regexp.last_match(0)
|
|
259
|
-
''
|
|
260
|
-
end
|
|
261
|
-
|
|
262
|
-
left_pad = ' ' * offset
|
|
263
|
-
left_pad += indent
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
# return "#{left_pad}#{prefix}#{color}#{self}#{last_color} #{note}" unless len.positive?
|
|
267
|
-
|
|
268
|
-
# Don't break inside of tag values
|
|
269
|
-
str = gsub(/@\S+\(.*?\)/) { |tag| tag.gsub(/\s/, '%%%%') }.gsub(/\n/, ' ')
|
|
270
|
-
|
|
271
|
-
words = str.split(/ /).map { |word| word.gsub(/%%%%/, ' ') }
|
|
272
|
-
out = []
|
|
273
|
-
line = []
|
|
274
|
-
|
|
275
|
-
words.each do |word|
|
|
276
|
-
if word.uncolor.length >= len
|
|
277
|
-
chars = word.uncolor.split('')
|
|
278
|
-
out << chars.slice!(0, len - 1).join('') while chars.count >= len
|
|
279
|
-
line << chars.join('')
|
|
280
|
-
next
|
|
281
|
-
elsif line.join(' ').uncolor.length + word.uncolor.length + 1 > len
|
|
282
|
-
out.push(line.join(' '))
|
|
283
|
-
line.clear
|
|
284
|
-
end
|
|
285
|
-
|
|
286
|
-
line << word.uncolor
|
|
287
|
-
end
|
|
288
|
-
out.push(line.join(' '))
|
|
289
|
-
|
|
290
|
-
last_color = ''
|
|
291
|
-
out[0] = format("%-#{pad}s%s%s", out[0], last_color, after)
|
|
292
|
-
|
|
293
|
-
out.map.with_index { |l, idx|
|
|
294
|
-
if !pad_first && idx == 0
|
|
295
|
-
"#{color}#{prefix}#{l}#{last_color}"
|
|
296
|
-
else
|
|
297
|
-
"#{left_pad}#{color}#{prefix}#{l}#{last_color}"
|
|
298
|
-
end
|
|
299
|
-
}.join("\n") + " #{note}".chomp
|
|
300
|
-
# res.join("\n").strip + last_color + " #{note}".chomp
|
|
301
|
-
end
|
|
302
|
-
|
|
303
|
-
##
|
|
304
|
-
## Capitalize on the first character on string
|
|
305
|
-
##
|
|
306
|
-
## @return Capitalized string
|
|
307
|
-
##
|
|
308
|
-
def cap_first
|
|
309
|
-
sub(/^\w/) do |m|
|
|
310
|
-
m.upcase
|
|
311
|
-
end
|
|
312
|
-
end
|
|
313
|
-
|
|
314
|
-
##
|
|
315
|
-
## Pluralize a string based on quantity
|
|
316
|
-
##
|
|
317
|
-
## @param number [Integer] the quantity of the
|
|
318
|
-
## object the string represents
|
|
319
|
-
##
|
|
320
|
-
def to_p(number)
|
|
321
|
-
number == 1 ? self : "#{self}s"
|
|
322
|
-
end
|
|
323
|
-
|
|
324
|
-
##
|
|
325
|
-
## Convert an age string to a qualified type
|
|
326
|
-
##
|
|
327
|
-
## @return [Symbol] :oldest or :newest
|
|
328
|
-
##
|
|
329
|
-
def normalize_age(default = :newest)
|
|
330
|
-
case self
|
|
331
|
-
when /^o/i
|
|
332
|
-
:oldest
|
|
333
|
-
when /^n/i
|
|
334
|
-
:newest
|
|
335
|
-
else
|
|
336
|
-
default
|
|
337
|
-
end
|
|
338
|
-
end
|
|
339
|
-
|
|
340
|
-
## @see #normalize_age
|
|
341
|
-
def normalize_age!(default = :newest)
|
|
342
|
-
replace normalize_age(default)
|
|
343
|
-
end
|
|
344
|
-
|
|
345
|
-
##
|
|
346
|
-
## Convert a sort order string to a qualified type
|
|
347
|
-
##
|
|
348
|
-
## @return [String] 'asc' or 'desc'
|
|
349
|
-
##
|
|
350
|
-
def normalize_order!(default = 'asc')
|
|
351
|
-
replace normalize_order(default)
|
|
352
|
-
end
|
|
353
|
-
|
|
354
|
-
def normalize_order(default = 'asc')
|
|
355
|
-
case self
|
|
356
|
-
when /^a/i
|
|
357
|
-
'asc'
|
|
358
|
-
when /^d/i
|
|
359
|
-
'desc'
|
|
360
|
-
else
|
|
361
|
-
default
|
|
362
|
-
end
|
|
363
|
-
end
|
|
364
|
-
|
|
365
|
-
##
|
|
366
|
-
## Convert a case sensitivity string to a symbol
|
|
367
|
-
##
|
|
368
|
-
## @return Symbol :smart, :sensitive, :ignore
|
|
369
|
-
##
|
|
370
|
-
def normalize_case(default = :smart)
|
|
371
|
-
case self
|
|
372
|
-
when /^(c|sens)/i
|
|
373
|
-
:sensitive
|
|
374
|
-
when /^i/i
|
|
375
|
-
:ignore
|
|
376
|
-
when /^s/i
|
|
377
|
-
:smart
|
|
378
|
-
else
|
|
379
|
-
default.is_a?(Symbol) ? default : default.normalize_case
|
|
380
|
-
end
|
|
381
|
-
end
|
|
382
|
-
|
|
383
|
-
## @see #normalize_case
|
|
384
|
-
def normalize_case!
|
|
385
|
-
replace normalize_case
|
|
386
|
-
end
|
|
387
|
-
|
|
388
|
-
##
|
|
389
|
-
## Convert a boolean string to a symbol
|
|
390
|
-
##
|
|
391
|
-
## @return Symbol :and, :or, or :not
|
|
392
|
-
##
|
|
393
|
-
def normalize_bool(default = :and)
|
|
394
|
-
case self
|
|
395
|
-
when /(and|all)/i
|
|
396
|
-
:and
|
|
397
|
-
when /(any|or)/i
|
|
398
|
-
:or
|
|
399
|
-
when /(not|none)/i
|
|
400
|
-
:not
|
|
401
|
-
when /^p/i
|
|
402
|
-
:pattern
|
|
403
|
-
else
|
|
404
|
-
default.is_a?(Symbol) ? default : default.normalize_bool
|
|
405
|
-
end
|
|
406
|
-
end
|
|
407
|
-
|
|
408
|
-
## @see #normalize_bool
|
|
409
|
-
def normalize_bool!(default = :and)
|
|
410
|
-
replace normalize_bool(default)
|
|
411
|
-
end
|
|
412
|
-
|
|
413
|
-
##
|
|
414
|
-
## Convert a matching configuration string to a symbol
|
|
415
|
-
##
|
|
416
|
-
## @param default [Symbol] the default matching
|
|
417
|
-
## type to return if the string
|
|
418
|
-
## doesn't match a known symbol
|
|
419
|
-
## @return Symbol :fuzzy, :pattern, :exact
|
|
420
|
-
##
|
|
421
|
-
def normalize_matching(default = :pattern)
|
|
422
|
-
case self
|
|
423
|
-
when /^f/i
|
|
424
|
-
:fuzzy
|
|
425
|
-
when /^p/i
|
|
426
|
-
:pattern
|
|
427
|
-
when /^e/i
|
|
428
|
-
:exact
|
|
429
|
-
else
|
|
430
|
-
default.is_a?(Symbol) ? default : default.normalize_matching
|
|
431
|
-
end
|
|
432
|
-
end
|
|
433
|
-
|
|
434
|
-
## @see #normalize_matching
|
|
435
|
-
def normalize_matching!(default = :pattern)
|
|
436
|
-
replace normalize_bool(default)
|
|
437
|
-
end
|
|
438
|
-
|
|
439
|
-
##
|
|
440
|
-
## Adds ?: to any parentheticals in a regular expression
|
|
441
|
-
## to avoid match groups
|
|
442
|
-
##
|
|
443
|
-
## @return [String] modified regular expression
|
|
444
|
-
##
|
|
445
|
-
def normalize_trigger
|
|
446
|
-
gsub(/\((?!\?:)/, '(?:').downcase
|
|
447
|
-
end
|
|
448
|
-
|
|
449
|
-
## @see #normalize_trigger
|
|
450
|
-
def normalize_trigger!
|
|
451
|
-
replace normalize_trigger
|
|
452
|
-
end
|
|
453
|
-
|
|
454
|
-
##
|
|
455
|
-
## Convert ? and * wildcards to regular expressions.
|
|
456
|
-
## Uses \S (non-whitespace) instead of . (any character)
|
|
457
|
-
##
|
|
458
|
-
## @return [String] Regular expression string
|
|
459
|
-
##
|
|
460
|
-
def wildcard_to_rx
|
|
461
|
-
gsub(/\?/, '\S').gsub(/\*/, '\S*?').gsub(/\]\]/, '--')
|
|
462
|
-
end
|
|
463
|
-
|
|
464
|
-
##
|
|
465
|
-
## Add @ prefix to string if needed, maintains +/- prefix
|
|
466
|
-
##
|
|
467
|
-
## @return [String] @string
|
|
468
|
-
##
|
|
469
|
-
def add_at
|
|
470
|
-
strip.sub(/^([+-]*)@?/, '\1@')
|
|
471
|
-
end
|
|
472
|
-
|
|
473
|
-
##
|
|
474
|
-
## Removes @ prefix if needed, maintains +/- prefix
|
|
475
|
-
##
|
|
476
|
-
## @return [String] string without @ prefix
|
|
477
|
-
##
|
|
478
|
-
def remove_at
|
|
479
|
-
strip.sub(/^([+-]*)@?/, '\1')
|
|
480
|
-
end
|
|
481
|
-
|
|
482
|
-
##
|
|
483
|
-
## Convert a list of tags to an array. Tags can be with
|
|
484
|
-
## or without @ symbols, separated by any character, and
|
|
485
|
-
## can include parenthetical values (with spaces)
|
|
486
|
-
##
|
|
487
|
-
## @return [Array] array of tags including @ symbols
|
|
488
|
-
##
|
|
489
|
-
def to_tags
|
|
490
|
-
gsub(/ *, */, ' ').scan(/(@?(?:\S+(?:\(.+\)))|@?(?:\S+))/).map(&:first).sort.uniq.map(&:add_at)
|
|
491
|
-
end
|
|
492
|
-
|
|
493
|
-
##
|
|
494
|
-
## @brief Adds tags to a string
|
|
495
|
-
##
|
|
496
|
-
## @param tags [String or Array] List of tags to add. @ symbol optional
|
|
497
|
-
## @param remove [Boolean] remove tags instead of adding
|
|
498
|
-
##
|
|
499
|
-
## @return [String] the tagged string
|
|
500
|
-
##
|
|
501
|
-
def add_tags(tags, remove: false)
|
|
502
|
-
title = self.dup
|
|
503
|
-
tags = tags.to_tags
|
|
504
|
-
tags.each { |tag| title.tag!(tag, remove: remove) }
|
|
505
|
-
title
|
|
506
|
-
end
|
|
507
|
-
|
|
508
|
-
## @see #add_tags
|
|
509
|
-
def add_tags!(tags, remove: false)
|
|
510
|
-
replace add_tags(tags, remove: remove)
|
|
511
|
-
end
|
|
512
|
-
|
|
513
|
-
##
|
|
514
|
-
## Add, rename, or remove a tag in place
|
|
515
|
-
##
|
|
516
|
-
## @see #tag
|
|
517
|
-
##
|
|
518
|
-
def tag!(tag, **options)
|
|
519
|
-
replace tag(tag, **options)
|
|
520
|
-
end
|
|
521
|
-
|
|
522
|
-
##
|
|
523
|
-
## Add, rename, or remove a tag
|
|
524
|
-
##
|
|
525
|
-
## @param tag The tag
|
|
526
|
-
## @param value [String] Value for tag (@tag(value))
|
|
527
|
-
## @param remove [Boolean] Remove the tag instead of adding
|
|
528
|
-
## @param rename_to [String] Replace tag with this tag
|
|
529
|
-
## @param regex [Boolean] Tag is regular expression
|
|
530
|
-
## @param single [Boolean] Operating on a single item (for logging)
|
|
531
|
-
## @param force [Boolean] With rename_to, add tag if it doesn't exist
|
|
532
|
-
##
|
|
533
|
-
## @return [String] The string with modified tags
|
|
534
|
-
##
|
|
535
|
-
def tag(tag, value: nil, remove: false, rename_to: nil, regex: false, single: false, force: false)
|
|
536
|
-
log_level = single ? :info : :debug
|
|
537
|
-
title = dup
|
|
538
|
-
title.chomp!
|
|
539
|
-
tag = tag.sub(/^@?/, '')
|
|
540
|
-
case_sensitive = tag !~ /[A-Z]/
|
|
541
|
-
|
|
542
|
-
rx_tag = if regex
|
|
543
|
-
tag.gsub(/\./, '\S')
|
|
544
|
-
else
|
|
545
|
-
tag.gsub(/\?/, '.').gsub(/\*/, '\S*?')
|
|
546
|
-
end
|
|
547
|
-
|
|
548
|
-
if remove || rename_to
|
|
549
|
-
rx = Regexp.new("(?<=^| )@#{rx_tag}(?<parens>\\((?<value>[^)]*)\\))?(?= |$)", case_sensitive)
|
|
550
|
-
m = title.match(rx)
|
|
551
|
-
|
|
552
|
-
if m.nil? && rename_to && force
|
|
553
|
-
title.tag!(rename_to, value: value, single: single)
|
|
554
|
-
elsif m
|
|
555
|
-
title.gsub!(rx) do
|
|
556
|
-
rename_to ? "@#{rename_to}#{value.nil? ? m['parens'] : "(#{value})"}" : ''
|
|
557
|
-
end
|
|
558
|
-
|
|
559
|
-
title.dedup_tags!
|
|
560
|
-
title.chomp!
|
|
561
|
-
|
|
562
|
-
if rename_to
|
|
563
|
-
f = "@#{tag}".cyan
|
|
564
|
-
t = "@#{rename_to}".cyan
|
|
565
|
-
Doing.logger.write(log_level, 'Tag:', %(renamed #{f} to #{t} in "#{title}"))
|
|
566
|
-
else
|
|
567
|
-
f = "@#{tag}".cyan
|
|
568
|
-
Doing.logger.write(log_level, 'Tag:', %(removed #{f} from "#{title}"))
|
|
569
|
-
end
|
|
570
|
-
else
|
|
571
|
-
Doing.logger.debug('Skipped:', "not tagged #{"@#{tag}".cyan}")
|
|
572
|
-
end
|
|
573
|
-
elsif title =~ /@#{tag}(?=[ (]|$)/
|
|
574
|
-
Doing.logger.debug('Skipped:', "already tagged #{"@#{tag}".cyan}")
|
|
575
|
-
return title
|
|
576
|
-
else
|
|
577
|
-
add = tag
|
|
578
|
-
add += "(#{value})" unless value.nil?
|
|
579
|
-
title.chomp!
|
|
580
|
-
title += " @#{add}"
|
|
581
|
-
|
|
582
|
-
title.dedup_tags!
|
|
583
|
-
title.chomp!
|
|
584
|
-
Doing.logger.write(log_level, 'Tag:', %(added #{('@' + tag).cyan} to "#{title}"))
|
|
585
|
-
end
|
|
586
|
-
|
|
587
|
-
title.gsub(/ +/, ' ')
|
|
588
|
-
end
|
|
589
|
-
|
|
590
|
-
##
|
|
591
|
-
## Remove duplicate tags, leaving only first occurrence
|
|
592
|
-
##
|
|
593
|
-
## @return Deduplicated string
|
|
594
|
-
##
|
|
595
|
-
def dedup_tags
|
|
596
|
-
title = dup
|
|
597
|
-
tags = title.scan(/(?<=\A| )(@(\S+?)(\([^)]+\))?)(?= |\Z)/).uniq
|
|
598
|
-
tags.each do |tag|
|
|
599
|
-
found = false
|
|
600
|
-
title.gsub!(/( |^)#{tag[1]}(\([^)]+\))?(?= |$)/) do |m|
|
|
601
|
-
if found
|
|
602
|
-
''
|
|
603
|
-
else
|
|
604
|
-
found = true
|
|
605
|
-
m
|
|
606
|
-
end
|
|
607
|
-
end
|
|
608
|
-
end
|
|
609
|
-
title
|
|
610
|
-
end
|
|
611
|
-
|
|
612
|
-
## @see #dedup_tags
|
|
613
|
-
def dedup_tags!
|
|
614
|
-
replace dedup_tags
|
|
615
|
-
end
|
|
616
|
-
|
|
617
|
-
# Returns the last escape sequence from a string.
|
|
618
|
-
#
|
|
619
|
-
# Actually returns all escape codes, with the assumption
|
|
620
|
-
# that the result of inserting them will generate the
|
|
621
|
-
# same color as was set at the end of the string.
|
|
622
|
-
# Because you can send modifiers like dark and bold
|
|
623
|
-
# separate from color codes, only using the last code
|
|
624
|
-
# may not render the same style.
|
|
625
|
-
#
|
|
626
|
-
# @return [String] All escape codes in string
|
|
627
|
-
#
|
|
628
|
-
def last_color
|
|
629
|
-
scan(/\e\[[\d;]+m/).join('')
|
|
630
|
-
end
|
|
631
|
-
|
|
632
|
-
##
|
|
633
|
-
## Turn raw urls into HTML links
|
|
634
|
-
##
|
|
635
|
-
## @param opt [Hash] Additional Options
|
|
636
|
-
##
|
|
637
|
-
## @option opt [Symbol] :format can be :markdown or
|
|
638
|
-
## :html (default)
|
|
639
|
-
##
|
|
640
|
-
def link_urls(**opt)
|
|
641
|
-
fmt = opt.fetch(:format, :html)
|
|
642
|
-
return self unless fmt
|
|
643
|
-
|
|
644
|
-
str = dup
|
|
645
|
-
|
|
646
|
-
str = str.remove_self_links if fmt == :markdown
|
|
647
|
-
|
|
648
|
-
str.replace_qualified_urls(format: fmt).clean_unlinked_urls
|
|
649
|
-
end
|
|
650
|
-
|
|
651
|
-
## @see #link_urls
|
|
652
|
-
def link_urls!(**opt)
|
|
653
|
-
fmt = opt.fetch(:format, :html)
|
|
654
|
-
replace link_urls(format: fmt)
|
|
655
|
-
end
|
|
656
|
-
|
|
657
|
-
# Remove <self-linked> formatting
|
|
658
|
-
def remove_self_links
|
|
659
|
-
gsub(/<(.*?)>/) do |match|
|
|
660
|
-
m = Regexp.last_match
|
|
661
|
-
if m[1] =~ /^https?:/
|
|
662
|
-
m[1]
|
|
663
|
-
else
|
|
664
|
-
match
|
|
665
|
-
end
|
|
666
|
-
end
|
|
667
|
-
end
|
|
668
|
-
|
|
669
|
-
# Replace qualified urls
|
|
670
|
-
def replace_qualified_urls(**options)
|
|
671
|
-
fmt = options.fetch(:format, :html)
|
|
672
|
-
gsub(%r{(?mi)(?x:
|
|
673
|
-
(?<!["'\[(\\])
|
|
674
|
-
(?<protocol>(?:http|https)://)
|
|
675
|
-
(?<domain>[\w\-]+(?:\.[\w\-]+)+)
|
|
676
|
-
(?<path>[\w\-.,@?^=%&;:/~+#]*[\w\-@^=%&;/~+#])?
|
|
677
|
-
)}) do |_match|
|
|
678
|
-
m = Regexp.last_match
|
|
679
|
-
url = "#{m['domain']}#{m['path']}"
|
|
680
|
-
proto = m['protocol'].nil? ? 'http://' : m['protocol']
|
|
681
|
-
case fmt
|
|
682
|
-
when :terminal
|
|
683
|
-
TTY::Link.link_to("#{proto}#{url}", "#{proto}#{url}")
|
|
684
|
-
when :html
|
|
685
|
-
%(<a href="#{proto}#{url}" title="Link to #{m['domain']}">[#{url}]</a>)
|
|
686
|
-
when :markdown
|
|
687
|
-
"[#{url}](#{proto}#{url})"
|
|
688
|
-
else
|
|
689
|
-
m[0]
|
|
690
|
-
end
|
|
691
|
-
end
|
|
692
|
-
end
|
|
693
|
-
|
|
694
|
-
# Clean up unlinked <urls>
|
|
695
|
-
def clean_unlinked_urls
|
|
696
|
-
gsub(/<(\w+:.*?)>/) do |match|
|
|
697
|
-
m = Regexp.last_match
|
|
698
|
-
if m[1] =~ /<a href/
|
|
699
|
-
match
|
|
700
|
-
else
|
|
701
|
-
%(<a href="#{m[1]}" title="Link to #{m[1]}">[link]</a>)
|
|
702
|
-
end
|
|
703
|
-
end
|
|
704
|
-
end
|
|
705
|
-
|
|
706
|
-
def to_bool
|
|
707
|
-
case self
|
|
708
|
-
when /^[yt1]/i
|
|
709
|
-
true
|
|
710
|
-
else
|
|
711
|
-
false
|
|
712
|
-
end
|
|
713
|
-
end
|
|
714
|
-
|
|
715
|
-
##
|
|
716
|
-
## Convert a string value to an appropriate type. If
|
|
717
|
-
## kind is not specified, '[one, two]' becomes an Array,
|
|
718
|
-
## '1' becomes Integer, '1.5' becomes Float, 'true' or
|
|
719
|
-
## 'yes' becomes TrueClass, 'false' or 'no' becomes
|
|
720
|
-
## FalseClass.
|
|
721
|
-
##
|
|
722
|
-
## @param kind [String] specify string, array,
|
|
723
|
-
## integer, float, symbol, or boolean
|
|
724
|
-
## (falls back to string if value is
|
|
725
|
-
## not recognized)
|
|
726
|
-
## @return Converted object type
|
|
727
|
-
def set_type(kind = nil)
|
|
728
|
-
if kind
|
|
729
|
-
case kind.to_s
|
|
730
|
-
when /^a/i
|
|
731
|
-
gsub(/^\[ *| *\]$/, '').split(/ *, */)
|
|
732
|
-
when /^i/i
|
|
733
|
-
to_i
|
|
734
|
-
when /^(fa|tr)/i
|
|
735
|
-
to_bool
|
|
736
|
-
when /^f/i
|
|
737
|
-
to_f
|
|
738
|
-
when /^sy/i
|
|
739
|
-
sub(/^:/, '').to_sym
|
|
740
|
-
when /^b/i
|
|
741
|
-
self =~ /^(true|yes)$/ ? true : false
|
|
742
|
-
else
|
|
743
|
-
to_s
|
|
744
|
-
end
|
|
745
|
-
else
|
|
746
|
-
case self
|
|
747
|
-
when /(^\[.*?\]$| *, *)/
|
|
748
|
-
gsub(/^\[ *| *\]$/, '').split(/ *, */)
|
|
749
|
-
when /^[0-9]+$/
|
|
750
|
-
to_i
|
|
751
|
-
when /^[0-9]+\.[0-9]+$/
|
|
752
|
-
to_f
|
|
753
|
-
when /^:\w+/
|
|
754
|
-
sub(/^:/, '').to_sym
|
|
755
|
-
when /^(true|yes)$/i
|
|
756
|
-
true
|
|
757
|
-
when /^(false|no)$/i
|
|
758
|
-
false
|
|
759
|
-
else
|
|
760
|
-
to_s
|
|
761
|
-
end
|
|
762
|
-
end
|
|
763
|
-
end
|
|
764
|
-
end
|
|
765
|
-
end
|