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