doing 2.1.26 → 2.1.27

Sign up to get free protection for your applications and to get access to all the features.
Files changed (151) hide show
  1. checksums.yaml +4 -4
  2. data/.yardoc/checksums +14 -19
  3. data/.yardoc/object_types +0 -0
  4. data/.yardoc/objects/root.dat +0 -0
  5. data/CHANGELOG.md +23 -0
  6. data/Dockerfile +5 -5
  7. data/Dockerfile-2.6 +5 -5
  8. data/Dockerfile-2.7 +5 -4
  9. data/Dockerfile-3.0 +5 -4
  10. data/Gemfile.lock +1 -1
  11. data/README.md +1 -1
  12. data/Rakefile +2 -3
  13. data/bin/commands/add_section.rb +2 -0
  14. data/bin/commands/again.rb +23 -65
  15. data/bin/commands/archive.rb +20 -61
  16. data/bin/commands/cancel.rb +27 -69
  17. data/bin/commands/changes.rb +32 -5
  18. data/bin/commands/colors.rb +4 -2
  19. data/bin/commands/commands.rb +4 -2
  20. data/bin/commands/commands_accepting.rb +62 -11
  21. data/bin/commands/completion.rb +10 -7
  22. data/bin/commands/config.rb +1 -1
  23. data/bin/commands/done.rb +3 -17
  24. data/bin/commands/finish.rb +7 -30
  25. data/bin/commands/flag.rb +15 -51
  26. data/bin/commands/grep.rb +12 -28
  27. data/bin/commands/import.rb +3 -33
  28. data/bin/commands/last.rb +3 -36
  29. data/bin/commands/meanwhile.rb +3 -13
  30. data/bin/commands/note.rb +13 -52
  31. data/bin/commands/now.rb +15 -21
  32. data/bin/commands/on.rb +3 -4
  33. data/bin/commands/recent.rb +3 -4
  34. data/bin/commands/redo.rb +6 -2
  35. data/bin/commands/reset.rb +19 -52
  36. data/bin/commands/rotate.rb +5 -36
  37. data/bin/commands/select.rb +23 -41
  38. data/bin/commands/show.rb +28 -74
  39. data/bin/commands/since.rb +3 -4
  40. data/bin/commands/tag.rb +4 -34
  41. data/bin/commands/tags.rb +5 -32
  42. data/bin/commands/today.rb +3 -4
  43. data/bin/commands/view.rb +36 -73
  44. data/bin/commands/yesterday.rb +4 -5
  45. data/bin/doing +150 -13
  46. data/docs/doc/Array.html +3 -502
  47. data/docs/doc/BooleanTermParser/Clause.html +1 -1
  48. data/docs/doc/BooleanTermParser/Operator.html +1 -1
  49. data/docs/doc/BooleanTermParser/Query.html +1 -1
  50. data/docs/doc/BooleanTermParser/QueryParser.html +1 -1
  51. data/docs/doc/BooleanTermParser/QueryTransformer.html +1 -1
  52. data/docs/doc/BooleanTermParser.html +1 -1
  53. data/docs/doc/Doing/Color.html +62 -56
  54. data/docs/doc/Doing/Completion.html +1 -1
  55. data/docs/doc/Doing/Configuration.html +35 -1
  56. data/docs/doc/Doing/Errors/DoingNoTraceError.html +1 -1
  57. data/docs/doc/Doing/Errors/DoingRuntimeError.html +1 -1
  58. data/docs/doc/Doing/Errors/DoingStandardError.html +1 -1
  59. data/docs/doc/Doing/Errors/EmptyInput.html +1 -1
  60. data/docs/doc/Doing/Errors/NoResults.html +1 -1
  61. data/docs/doc/Doing/Errors/PluginException.html +1 -1
  62. data/docs/doc/Doing/Errors/UserCancelled.html +1 -1
  63. data/docs/doc/Doing/Errors/WrongCommand.html +1 -1
  64. data/docs/doc/Doing/Errors.html +1 -1
  65. data/docs/doc/Doing/Hooks.html +1 -1
  66. data/docs/doc/Doing/Item.html +1 -1
  67. data/docs/doc/Doing/Items.html +2 -2
  68. data/docs/doc/Doing/LogAdapter.html +1 -1
  69. data/docs/doc/Doing/Note.html +2 -2
  70. data/docs/doc/Doing/Pager.html +1 -1
  71. data/docs/doc/Doing/Plugins.html +1 -1
  72. data/docs/doc/Doing/Prompt.html +1 -1
  73. data/docs/doc/Doing/Section.html +1 -1
  74. data/docs/doc/Doing/TemplateString.html +2 -2
  75. data/docs/doc/Doing/Types.html +41 -1
  76. data/docs/doc/Doing/Util/Backup.html +1 -1
  77. data/docs/doc/Doing/Util.html +1 -1
  78. data/docs/doc/Doing/WWID.html +10 -10
  79. data/docs/doc/Doing.html +3 -3
  80. data/docs/doc/FalseClass.html +35 -1
  81. data/docs/doc/GLI/Commands/Help.html +1 -1
  82. data/docs/doc/GLI/Commands/MarkdownDocumentListener.html +1 -1
  83. data/docs/doc/GLI/Commands.html +1 -1
  84. data/docs/doc/GLI.html +1 -1
  85. data/docs/doc/Hash.html +1 -1
  86. data/docs/doc/Object.html +1 -1
  87. data/docs/doc/PhraseParser/Operator.html +1 -1
  88. data/docs/doc/PhraseParser/PhraseClause.html +1 -1
  89. data/docs/doc/PhraseParser/Query.html +1 -1
  90. data/docs/doc/PhraseParser/QueryParser.html +1 -1
  91. data/docs/doc/PhraseParser/QueryTransformer.html +1 -1
  92. data/docs/doc/PhraseParser/TermClause.html +1 -1
  93. data/docs/doc/PhraseParser.html +1 -1
  94. data/docs/doc/Status.html +1 -1
  95. data/docs/doc/String.html +287 -3155
  96. data/docs/doc/Symbol.html +40 -6
  97. data/docs/doc/Time.html +1 -1
  98. data/docs/doc/TrueClass.html +35 -1
  99. data/docs/doc/_index.html +5 -10
  100. data/docs/doc/class_list.html +1 -1
  101. data/docs/doc/file.README.html +2 -2
  102. data/docs/doc/index.html +2 -2
  103. data/docs/doc/method_list.html +278 -678
  104. data/docs/doc/top-level-namespace.html +2 -2
  105. data/doing.rdoc +277 -175
  106. data/lib/completion/_doing.zsh +33 -29
  107. data/lib/completion/doing.bash +30 -19
  108. data/lib/completion/doing.fish +84 -72
  109. data/lib/doing/array/array.rb +4 -0
  110. data/lib/doing/array/nested_hash.rb +17 -0
  111. data/lib/doing/{array.rb → array/tags.rb} +7 -25
  112. data/lib/doing/changelog/change.rb +26 -11
  113. data/lib/doing/changelog/changes.rb +13 -3
  114. data/lib/doing/{array_chronify.rb → chronify/array.rb} +0 -0
  115. data/lib/doing/chronify/chronify.rb +5 -0
  116. data/lib/doing/{numeric_chronify.rb → chronify/numeric.rb} +0 -0
  117. data/lib/doing/{string_chronify.rb → chronify/string.rb} +0 -0
  118. data/lib/doing/colors.rb +115 -54
  119. data/lib/doing/configuration.rb +4 -0
  120. data/lib/doing/good.rb +8 -0
  121. data/lib/doing/help_monkey_patch.rb +6 -5
  122. data/lib/doing/item.rb +5 -5
  123. data/lib/doing/items.rb +2 -2
  124. data/lib/doing/log_adapter.rb +35 -2
  125. data/lib/doing/normalize.rb +188 -0
  126. data/lib/doing/plugins/export/dayone_export.rb +1 -1
  127. data/lib/doing/plugins/export/html_export.rb +1 -1
  128. data/lib/doing/plugins/export/json_export.rb +1 -1
  129. data/lib/doing/plugins/export/markdown_export.rb +1 -1
  130. data/lib/doing/plugins/export/template_export.rb +3 -1
  131. data/lib/doing/prompt.rb +1 -3
  132. data/lib/doing/string/highlight.rb +95 -0
  133. data/lib/doing/string/query.rb +129 -0
  134. data/lib/doing/string/string.rb +12 -0
  135. data/lib/doing/string/tags.rb +164 -0
  136. data/lib/doing/string/transform.rb +168 -0
  137. data/lib/doing/string/truncate.rb +75 -0
  138. data/lib/doing/string/url.rb +82 -0
  139. data/lib/doing/template_string.rb +0 -22
  140. data/lib/doing/types.rb +8 -0
  141. data/lib/doing/util.rb +13 -9
  142. data/lib/doing/version.rb +1 -1
  143. data/lib/doing/wwid.rb +53 -35
  144. data/lib/doing.rb +4 -6
  145. data/lib/examples/plugins/wiki_export/wiki_export.rb +1 -1
  146. data/lib/helpers/threaded_tests.rb +15 -2
  147. data/scripts/deploy.rb +107 -0
  148. data/scripts/runtests.sh +4 -0
  149. metadata +19 -8
  150. data/lib/doing/string.rb +0 -765
  151. 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