doing 2.1.26 → 2.1.30
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.yardoc/checksums +15 -20
- data/.yardoc/object_types +0 -0
- data/.yardoc/objects/root.dat +0 -0
- data/CHANGELOG.md +52 -0
- 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 +2 -1
- data/README.md +1 -1
- data/Rakefile +2 -3
- data/bin/commands/add_section.rb +2 -0
- data/bin/commands/again.rb +23 -65
- data/bin/commands/archive.rb +20 -61
- data/bin/commands/cancel.rb +27 -69
- data/bin/commands/changes.rb +53 -12
- data/bin/commands/colors.rb +4 -2
- data/bin/commands/commands.rb +4 -2
- data/bin/commands/commands_accepting.rb +62 -11
- data/bin/commands/completion.rb +10 -7
- data/bin/commands/config.rb +8 -8
- data/bin/commands/done.rb +3 -17
- data/bin/commands/finish.rb +7 -30
- data/bin/commands/flag.rb +15 -51
- data/bin/commands/grep.rb +12 -28
- data/bin/commands/import.rb +3 -33
- data/bin/commands/last.rb +3 -36
- data/bin/commands/meanwhile.rb +3 -13
- data/bin/commands/note.rb +13 -52
- data/bin/commands/now.rb +15 -21
- data/bin/commands/on.rb +3 -4
- data/bin/commands/open.rb +3 -3
- data/bin/commands/recent.rb +3 -4
- data/bin/commands/redo.rb +6 -2
- data/bin/commands/reset.rb +19 -52
- data/bin/commands/rotate.rb +5 -36
- data/bin/commands/select.rb +23 -41
- data/bin/commands/show.rb +28 -74
- data/bin/commands/since.rb +3 -4
- data/bin/commands/tag.rb +4 -34
- data/bin/commands/tags.rb +5 -32
- data/bin/commands/today.rb +3 -4
- data/bin/commands/view.rb +36 -73
- data/bin/commands/yesterday.rb +4 -5
- data/bin/doing +150 -13
- data/docs/doc/Array.html +3 -502
- data/docs/doc/BooleanTermParser/Clause.html +1 -1
- data/docs/doc/BooleanTermParser/Operator.html +1 -1
- data/docs/doc/BooleanTermParser/Query.html +1 -1
- data/docs/doc/BooleanTermParser/QueryParser.html +1 -1
- data/docs/doc/BooleanTermParser/QueryTransformer.html +1 -1
- data/docs/doc/BooleanTermParser.html +1 -1
- data/docs/doc/Doing/Color.html +62 -56
- data/docs/doc/Doing/Completion.html +1 -1
- data/docs/doc/Doing/Configuration.html +35 -1
- data/docs/doc/Doing/Errors/DoingNoTraceError.html +1 -1
- data/docs/doc/Doing/Errors/DoingRuntimeError.html +1 -1
- data/docs/doc/Doing/Errors/DoingStandardError.html +1 -1
- data/docs/doc/Doing/Errors/EmptyInput.html +1 -1
- data/docs/doc/Doing/Errors/NoResults.html +1 -1
- data/docs/doc/Doing/Errors/PluginException.html +1 -1
- data/docs/doc/Doing/Errors/UserCancelled.html +1 -1
- data/docs/doc/Doing/Errors/WrongCommand.html +1 -1
- data/docs/doc/Doing/Errors.html +1 -1
- data/docs/doc/Doing/Hooks.html +1 -1
- data/docs/doc/Doing/Item.html +1 -1
- data/docs/doc/Doing/Items.html +2 -2
- data/docs/doc/Doing/LogAdapter.html +1 -1
- data/docs/doc/Doing/Note.html +2 -2
- data/docs/doc/Doing/Pager.html +1 -1
- data/docs/doc/Doing/Plugins.html +1 -1
- data/docs/doc/Doing/Prompt.html +1 -1
- data/docs/doc/Doing/Section.html +1 -1
- data/docs/doc/Doing/TemplateString.html +2 -2
- data/docs/doc/Doing/Types.html +41 -1
- data/docs/doc/Doing/Util/Backup.html +1 -1
- data/docs/doc/Doing/Util.html +1 -1
- data/docs/doc/Doing/WWID.html +10 -10
- data/docs/doc/Doing.html +3 -3
- data/docs/doc/FalseClass.html +35 -1
- data/docs/doc/GLI/Commands/Help.html +1 -1
- data/docs/doc/GLI/Commands/MarkdownDocumentListener.html +1 -1
- data/docs/doc/GLI/Commands.html +1 -1
- data/docs/doc/GLI.html +1 -1
- data/docs/doc/Hash.html +1 -1
- data/docs/doc/Object.html +1 -1
- data/docs/doc/PhraseParser/Operator.html +1 -1
- data/docs/doc/PhraseParser/PhraseClause.html +1 -1
- data/docs/doc/PhraseParser/Query.html +1 -1
- data/docs/doc/PhraseParser/QueryParser.html +1 -1
- data/docs/doc/PhraseParser/QueryTransformer.html +1 -1
- data/docs/doc/PhraseParser/TermClause.html +1 -1
- data/docs/doc/PhraseParser.html +1 -1
- data/docs/doc/Status.html +1 -1
- data/docs/doc/String.html +287 -3155
- data/docs/doc/Symbol.html +40 -6
- data/docs/doc/Time.html +1 -1
- data/docs/doc/TrueClass.html +35 -1
- data/docs/doc/_index.html +5 -10
- 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 +278 -678
- data/docs/doc/top-level-namespace.html +2 -2
- data/doing.gemspec +1 -0
- data/doing.rdoc +297 -206
- data/lib/completion/_doing.zsh +32 -32
- data/lib/completion/doing.bash +30 -30
- data/lib/completion/doing.fish +87 -77
- 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 +37 -8
- data/lib/doing/changelog/version.rb +11 -3
- 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/completion/zsh_completion.rb +5 -0
- data/lib/doing/configuration.rb +9 -5
- data/lib/doing/good.rb +8 -0
- data/lib/doing/help_monkey_patch.rb +6 -5
- data/lib/doing/item.rb +5 -5
- 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/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/prompt.rb +1 -3
- data/lib/doing/section.rb +1 -1
- 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 +0 -22
- data/lib/doing/types.rb +8 -0
- data/lib/doing/util.rb +13 -9
- data/lib/doing/version.rb +1 -1
- data/lib/doing/wwid.rb +54 -36
- data/lib/doing.rb +5 -6
- data/lib/examples/plugins/wiki_export/wiki_export.rb +1 -1
- data/lib/helpers/threaded_tests.rb +15 -2
- data/scripts/deploy.rb +107 -0
- data/scripts/runtests.sh +4 -0
- metadata +39 -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.good?
|
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
|