doing 2.1.25 → 2.1.29

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 (163) hide show
  1. checksums.yaml +4 -4
  2. data/.yardoc/checksums +15 -20
  3. data/.yardoc/object_types +0 -0
  4. data/.yardoc/objects/root.dat +0 -0
  5. data/CHANGELOG.md +322 -108
  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 +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 +83 -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 -3706
  55. data/docs/doc/Array.html +3 -502
  56. data/docs/doc/BooleanTermParser/Clause.html +1 -1
  57. data/docs/doc/BooleanTermParser/Operator.html +1 -1
  58. data/docs/doc/BooleanTermParser/Query.html +1 -1
  59. data/docs/doc/BooleanTermParser/QueryParser.html +1 -1
  60. data/docs/doc/BooleanTermParser/QueryTransformer.html +1 -1
  61. data/docs/doc/BooleanTermParser.html +1 -1
  62. data/docs/doc/Doing/Color.html +62 -56
  63. data/docs/doc/Doing/Completion.html +1 -1
  64. data/docs/doc/Doing/Configuration.html +36 -1
  65. data/docs/doc/Doing/Errors/DoingNoTraceError.html +1 -1
  66. data/docs/doc/Doing/Errors/DoingRuntimeError.html +1 -1
  67. data/docs/doc/Doing/Errors/DoingStandardError.html +1 -1
  68. data/docs/doc/Doing/Errors/EmptyInput.html +1 -1
  69. data/docs/doc/Doing/Errors/NoResults.html +1 -1
  70. data/docs/doc/Doing/Errors/PluginException.html +1 -1
  71. data/docs/doc/Doing/Errors/UserCancelled.html +1 -1
  72. data/docs/doc/Doing/Errors/WrongCommand.html +1 -1
  73. data/docs/doc/Doing/Errors.html +1 -1
  74. data/docs/doc/Doing/Hooks.html +1 -1
  75. data/docs/doc/Doing/Item.html +1 -1
  76. data/docs/doc/Doing/Items.html +2 -2
  77. data/docs/doc/Doing/LogAdapter.html +1 -1
  78. data/docs/doc/Doing/Note.html +2 -2
  79. data/docs/doc/Doing/Pager.html +1 -1
  80. data/docs/doc/Doing/Plugins.html +1 -1
  81. data/docs/doc/Doing/Prompt.html +46 -1
  82. data/docs/doc/Doing/Section.html +1 -1
  83. data/docs/doc/Doing/TemplateString.html +2 -2
  84. data/docs/doc/Doing/Types.html +41 -1
  85. data/docs/doc/Doing/Util/Backup.html +1 -1
  86. data/docs/doc/Doing/Util.html +1 -1
  87. data/docs/doc/Doing/WWID.html +10 -10
  88. data/docs/doc/Doing.html +3 -3
  89. data/docs/doc/FalseClass.html +235 -0
  90. data/docs/doc/GLI/Commands/Help.html +1 -1
  91. data/docs/doc/GLI/Commands/MarkdownDocumentListener.html +1 -1
  92. data/docs/doc/GLI/Commands.html +1 -1
  93. data/docs/doc/GLI.html +1 -1
  94. data/docs/doc/Hash.html +1 -1
  95. data/docs/doc/Numeric.html +1 -1
  96. data/docs/doc/Object.html +203 -0
  97. data/docs/doc/PhraseParser/Operator.html +1 -1
  98. data/docs/doc/PhraseParser/PhraseClause.html +1 -1
  99. data/docs/doc/PhraseParser/Query.html +1 -1
  100. data/docs/doc/PhraseParser/QueryParser.html +1 -1
  101. data/docs/doc/PhraseParser/QueryTransformer.html +1 -1
  102. data/docs/doc/PhraseParser/TermClause.html +1 -1
  103. data/docs/doc/PhraseParser.html +1 -1
  104. data/docs/doc/Status.html +1 -1
  105. data/docs/doc/String.html +287 -3155
  106. data/docs/doc/Symbol.html +40 -6
  107. data/docs/doc/Time.html +1 -1
  108. data/docs/doc/TrueClass.html +235 -0
  109. data/docs/doc/_index.html +5 -10
  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 +289 -681
  114. data/docs/doc/top-level-namespace.html +2 -2
  115. data/doing.rdoc +306 -205
  116. data/lib/completion/_doing.zsh +35 -35
  117. data/lib/completion/doing.bash +30 -30
  118. data/lib/completion/doing.fish +88 -78
  119. data/lib/doing/array/array.rb +4 -0
  120. data/lib/doing/array/nested_hash.rb +17 -0
  121. data/lib/doing/{array.rb → array/tags.rb} +7 -25
  122. data/lib/doing/changelog/change.rb +26 -11
  123. data/lib/doing/changelog/changes.rb +31 -4
  124. data/lib/doing/{array_chronify.rb → chronify/array.rb} +0 -0
  125. data/lib/doing/chronify/chronify.rb +5 -0
  126. data/lib/doing/{numeric_chronify.rb → chronify/numeric.rb} +0 -0
  127. data/lib/doing/{string_chronify.rb → chronify/string.rb} +0 -0
  128. data/lib/doing/colors.rb +115 -54
  129. data/lib/doing/configuration.rb +5 -0
  130. data/lib/doing/good.rb +8 -0
  131. data/lib/doing/help_monkey_patch.rb +6 -5
  132. data/lib/doing/item.rb +5 -5
  133. data/lib/doing/items.rb +2 -2
  134. data/lib/doing/log_adapter.rb +35 -2
  135. data/lib/doing/normalize.rb +188 -0
  136. data/lib/doing/pager.rb +1 -0
  137. data/lib/doing/plugins/export/dayone_export.rb +1 -1
  138. data/lib/doing/plugins/export/html_export.rb +1 -1
  139. data/lib/doing/plugins/export/json_export.rb +1 -1
  140. data/lib/doing/plugins/export/markdown_export.rb +1 -1
  141. data/lib/doing/plugins/export/template_export.rb +3 -1
  142. data/lib/doing/prompt.rb +9 -3
  143. data/lib/doing/string/highlight.rb +95 -0
  144. data/lib/doing/string/query.rb +129 -0
  145. data/lib/doing/string/string.rb +12 -0
  146. data/lib/doing/string/tags.rb +164 -0
  147. data/lib/doing/string/transform.rb +168 -0
  148. data/lib/doing/string/truncate.rb +75 -0
  149. data/lib/doing/string/url.rb +82 -0
  150. data/lib/doing/template_string.rb +0 -22
  151. data/lib/doing/types.rb +8 -0
  152. data/lib/doing/util.rb +13 -9
  153. data/lib/doing/version.rb +1 -1
  154. data/lib/doing/wwid.rb +53 -35
  155. data/lib/doing.rb +4 -6
  156. data/lib/examples/commands/wiki.rb +6 -7
  157. data/lib/examples/plugins/wiki_export/wiki_export.rb +1 -1
  158. data/lib/helpers/threaded_tests.rb +39 -20
  159. data/scripts/deploy.rb +107 -0
  160. data/scripts/runtests.sh +4 -0
  161. metadata +63 -8
  162. data/lib/doing/string.rb +0 -765
  163. data/lib/doing/symbol.rb +0 -28
@@ -72,7 +72,7 @@ module Doing
72
72
  self.template(nil)
73
73
  end
74
74
 
75
- totals = opt[:totals] ? wwid.tag_times(format: :markdown, sort_by_name: opt[:sort_tags], sort_order: opt[:tag_order]) : ''
75
+ totals = opt[:totals] ? wwid.tag_times(format: :markdown, sort_by: opt[:sort_tags], sort_order: opt[:tag_order]) : ''
76
76
 
77
77
  mdx = MarkdownRenderer.new(variables[:page_title], all_items, totals)
78
78
  Doing.logger.debug('Markdown Export:', "#{all_items.count} items output to Markdown")
@@ -17,6 +17,7 @@ module Doing
17
17
  end
18
18
 
19
19
  def self.render(wwid, items, variables: {})
20
+ # Doing.logger.benchmark(:template_render, :start)
20
21
  return if items.nil?
21
22
 
22
23
  opt = variables[:options]
@@ -133,9 +134,10 @@ module Doing
133
134
  # Doing.logger.debug('Template Export:', "#{items.count} items output to template #{opt[:template]}")
134
135
  if opt[:totals]
135
136
  out += wwid.tag_times(format: wwid.config['timer_format'].to_sym,
136
- sort_by_name: opt[:sort_tags],
137
+ sort_by: opt[:sort_tags],
137
138
  sort_order: opt[:tag_order])
138
139
  end
140
+ # Doing.logger.benchmark(:template_render, :finish)
139
141
  out
140
142
  end
141
143
 
data/lib/doing/prompt.rb CHANGED
@@ -8,6 +8,14 @@ module Doing
8
8
 
9
9
  include Color
10
10
 
11
+ ##
12
+ ## Clear the terminal screen
13
+ ##
14
+ def clear_screen(msg = nil)
15
+ puts "\e[H\e[2J" if STDOUT.tty?
16
+ puts msg if msg.good?
17
+ end
18
+
11
19
  def force_answer
12
20
  @force_answer ||= nil
13
21
  end
@@ -99,9 +107,7 @@ module Doing
99
107
  ## @return (Bool) yes or no
100
108
  ##
101
109
  def yn(question, default_response: false)
102
- unless @force_answer.nil?
103
- return @force_answer
104
- end
110
+ return @force_answer == :yes ? true : false unless @force_answer.nil?
105
111
 
106
112
  $stdin.reopen('/dev/tty')
107
113
 
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doing
4
+ ## Tag and search highlighting
5
+ class ::String
6
+ ## @param (see #highlight_tags)
7
+ def highlight_tags!(color = 'yellow', last_color: nil)
8
+ replace highlight_tags(color)
9
+ end
10
+
11
+ ##
12
+ ## Colorize @tags with ANSI escapes
13
+ ##
14
+ ## @param color [String] color (see #Color)
15
+ ##
16
+ ## @return [String] string with @tags highlighted
17
+ ##
18
+ def highlight_tags(color = 'yellow', last_color: nil)
19
+ unless last_color
20
+ escapes = scan(/(\e\[[\d;]+m)[^\e]+@/)
21
+ color = color.split(' ') unless color.is_a?(Array)
22
+ tag_color = color.each_with_object([]) { |c, arr| arr << Doing::Color.send(c) }.join('')
23
+ last_color = if escapes.good?
24
+ (escapes.count > 1 ? escapes[-2..-1] : [escapes[-1]]).map { |v| v[0] }.join('')
25
+ else
26
+ Doing::Color.default
27
+ end
28
+ end
29
+ gsub(/(\s|m)(@[^ ("']+)/, "\\1#{tag_color}\\2#{last_color}")
30
+ end
31
+
32
+ def highlight_search!(search, distance: nil, negate: false, case_type: nil)
33
+ replace highlight_search(search, distance: distance, negate: negate, case_type: case_type)
34
+ end
35
+
36
+ def highlight_search(search, distance: nil, negate: false, case_type: nil)
37
+ out = dup
38
+ prefs = Doing.config.settings['search'] || {}
39
+ matching = prefs.fetch('matching', 'pattern').normalize_matching
40
+ distance ||= prefs.fetch('distance', 3).to_i
41
+ case_type ||= prefs.fetch('case', 'smart').normalize_case
42
+
43
+ if search.rx? || matching == :fuzzy
44
+ rx = search.to_rx(distance: distance, case_type: case_type)
45
+ out.gsub!(rx) { |m| m.bgyellow.black }
46
+ else
47
+ query = search.strip.to_phrase_query
48
+
49
+ if query[:must].nil? && query[:must_not].nil?
50
+ query[:must] = query[:should]
51
+ query[:should] = []
52
+ end
53
+ qs = []
54
+ qs.concat(query[:must]) if query[:must]
55
+ qs.concat(query[:should]) if query[:should]
56
+ qs.each do |s|
57
+ rx = Regexp.new(s.wildcard_to_rx, ignore_case(s, case_type))
58
+ out.gsub!(rx) { |m| m.bgyellow.black }
59
+ end
60
+ end
61
+ out
62
+ end
63
+
64
+ # Returns the last escape sequence from a string.
65
+ #
66
+ # Actually returns all escape codes, with the assumption
67
+ # that the result of inserting them will generate the
68
+ # same color as was set at the end of the string.
69
+ # Because you can send modifiers like dark and bold
70
+ # separate from color codes, only using the last code
71
+ # may not render the same style.
72
+ #
73
+ # @return [String] All escape codes in string
74
+ #
75
+ def last_color
76
+ scan(/\e\[[\d;]+m/).join('')
77
+ end
78
+
79
+ ##
80
+ ## Remove color escape codes
81
+ ##
82
+ ## @return clean string
83
+ ##
84
+ def uncolor
85
+ gsub(/\e\[[\d;]+m/, '')
86
+ end
87
+
88
+ ##
89
+ ## @see #uncolor
90
+ ##
91
+ def uncolor!
92
+ replace uncolor
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doing
4
+ ## Handling of search and regex strings
5
+ class ::String
6
+ ##
7
+ ## Determine whether case should be ignored for string
8
+ ##
9
+ ## @param search The search string
10
+ ## @param case_type The case type, :smart,
11
+ ## :sensitive, :ignore
12
+ ##
13
+ ## @return [Boolean] true if case should be ignored
14
+ ##
15
+ def ignore_case(search, case_type)
16
+ (case_type == :smart && search !~ /[A-Z]/) || case_type == :ignore
17
+ end
18
+
19
+ ##
20
+ ## Test if line should be ignored
21
+ ##
22
+ ## @return [Boolean] line is empty or comment
23
+ ##
24
+ def ignore?
25
+ line = self
26
+ line =~ /^#/ || line =~ /^\s*$/
27
+ end
28
+
29
+ ##
30
+ ## Determines if receiver is surrounded by slashes or starts with single quote
31
+ ##
32
+ ## @return [Boolean] True if regex, False otherwise.
33
+ ##
34
+ def rx?
35
+ self =~ %r{(^/.*?/$|^')}
36
+ end
37
+
38
+ ##
39
+ ## Convert ? and * wildcards to regular expressions.
40
+ ## Uses \S (non-whitespace) instead of . (any character)
41
+ ##
42
+ ## @return [String] Regular expression string
43
+ ##
44
+ def wildcard_to_rx
45
+ gsub(/\?/, '\S').gsub(/\*/, '\S*?').gsub(/\]\]/, '--')
46
+ end
47
+
48
+ ##
49
+ ## Convert string to fuzzy regex. Characters in words
50
+ ## can be separated by up to *distance* characters in
51
+ ## haystack, spaces indicate unlimited distance.
52
+ ##
53
+ ## @example
54
+ ## "this word".to_rx(3)
55
+ ## # => /t.{0,3}h.{0,3}i.{0,3}s.{0,3}.*?w.{0,3}o.{0,3}r.{0,3}d/
56
+ ##
57
+ ## @param distance [Integer] Allowed distance
58
+ ## between characters
59
+ ## @param case_type The case type
60
+ ##
61
+ ## @return [Regexp] Regex pattern
62
+ ##
63
+ def to_rx(distance: nil, case_type: nil)
64
+ distance ||= Doing.config.fetch('search', 'distance', 3).to_i
65
+ case_type ||= Doing.config.fetch('search', 'case', 'smart')&.normalize_case
66
+ case_sensitive = case case_type
67
+ when :smart
68
+ self =~ /[A-Z]/ ? true : false
69
+ when :sensitive
70
+ true
71
+ else
72
+ false
73
+ end
74
+
75
+ pattern = case dup.strip
76
+ when %r{^/.*?/$}
77
+ sub(%r{/(.*?)/}, '\1')
78
+ when /^'/
79
+ sub(/^'(.*?)'?$/, '\1')
80
+ else
81
+ split(/ +/).map do |w|
82
+ w.split('').join(".{0,#{distance}}").gsub(/\+/, '\+').wildcard_to_rx
83
+ end.join('.*?')
84
+ end
85
+ Regexp.new(pattern, !case_sensitive)
86
+ end
87
+
88
+ def to_phrase_query
89
+ parser = PhraseParser::QueryParser.new
90
+ transformer = PhraseParser::QueryTransformer.new
91
+ parse_tree = parser.parse(self)
92
+ transformer.apply(parse_tree).to_elasticsearch
93
+ end
94
+
95
+ def to_query
96
+ parser = BooleanTermParser::QueryParser.new
97
+ transformer = BooleanTermParser::QueryTransformer.new
98
+ parse_tree = parser.parse(self)
99
+ transformer.apply(parse_tree).to_elasticsearch
100
+ end
101
+
102
+ ##
103
+ ## Test string for truthiness (0, "f", "false", "n", "no" all return false, case insensitive, otherwise true)
104
+ ##
105
+ ## @return [Boolean] String is truthy
106
+ ##
107
+ def truthy?
108
+ if self =~ /^(0|f(alse)?|n(o)?)$/i
109
+ false
110
+ else
111
+ true
112
+ end
113
+ end
114
+
115
+ ##
116
+ ## Returns a bool representation of the string.
117
+ ##
118
+ ## @return [Boolean] Bool representation of the object.
119
+ ##
120
+ def to_bool
121
+ case self
122
+ when /^[yt1]/i
123
+ true
124
+ else
125
+ false
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ::String
4
+ include Doing::Color
5
+ end
6
+
7
+ require_relative 'highlight'
8
+ require_relative 'query'
9
+ require_relative 'tags'
10
+ require_relative 'transform'
11
+ require_relative 'truncate'
12
+ require_relative 'url'
@@ -0,0 +1,164 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doing
4
+ # Handling of @tags in strings
5
+ class ::String
6
+ ##
7
+ ## Add @ prefix to string if needed, maintains +/- prefix
8
+ ##
9
+ ## @return [String] @string
10
+ ##
11
+ def add_at
12
+ strip.sub(/^([+-]*)@?/, '\1@')
13
+ end
14
+
15
+ ##
16
+ ## Removes @ prefix if needed, maintains +/- prefix
17
+ ##
18
+ ## @return [String] string without @ prefix
19
+ ##
20
+ def remove_at
21
+ strip.sub(/^([+-]*)@?/, '\1')
22
+ end
23
+
24
+ ##
25
+ ## Convert a list of tags to an array. Tags can be with
26
+ ## or without @ symbols, separated by any character, and
27
+ ## can include parenthetical values (with spaces)
28
+ ##
29
+ ## @return [Array] array of tags including @ symbols
30
+ ##
31
+ def to_tags
32
+ arr = gsub(/ *, */, ' ').scan(/(@?(?:\S+(?:\(.+\)))|@?(?:\S+))/).map(&:first).sort.uniq.map(&:add_at)
33
+ if block_given?
34
+ yield arr
35
+ else
36
+ arr
37
+ end
38
+ end
39
+
40
+ ##
41
+ ## @brief Adds tags to a string
42
+ ##
43
+ ## @param tags [String or Array] List of tags to add. @ symbol optional
44
+ ## @param remove [Boolean] remove tags instead of adding
45
+ ##
46
+ ## @return [String] the tagged string
47
+ ##
48
+ def add_tags(tags, remove: false)
49
+ title = dup
50
+ tags = tags.to_tags
51
+ tags.each { |tag| title.tag!(tag, remove: remove) }
52
+ title
53
+ end
54
+
55
+ ## @see #add_tags
56
+ def add_tags!(tags, remove: false)
57
+ replace add_tags(tags, remove: remove)
58
+ end
59
+
60
+ ##
61
+ ## Add, rename, or remove a tag in place
62
+ ##
63
+ ## @see #tag
64
+ ##
65
+ def tag!(tag, **options)
66
+ replace tag(tag, **options)
67
+ end
68
+
69
+ ##
70
+ ## Add, rename, or remove a tag
71
+ ##
72
+ ## @param tag The tag
73
+ ## @param value [String] Value for tag (@tag(value))
74
+ ## @param remove [Boolean] Remove the tag instead of adding
75
+ ## @param rename_to [String] Replace tag with this tag
76
+ ## @param regex [Boolean] Tag is regular expression
77
+ ## @param single [Boolean] Operating on a single item (for logging)
78
+ ## @param force [Boolean] With rename_to, add tag if it doesn't exist
79
+ ##
80
+ ## @return [String] The string with modified tags
81
+ ##
82
+ def tag(tag, value: nil, remove: false, rename_to: nil, regex: false, single: false, force: false)
83
+ log_level = single ? :info : :debug
84
+ title = dup
85
+ title.chomp!
86
+ tag = tag.sub(/^@?/, '')
87
+ case_sensitive = tag !~ /[A-Z]/
88
+
89
+ rx_tag = if regex
90
+ tag.gsub(/\./, '\S')
91
+ else
92
+ tag.gsub(/\?/, '.').gsub(/\*/, '\S*?')
93
+ end
94
+
95
+ if remove || rename_to
96
+ rx = Regexp.new("(?<=^| )@#{rx_tag}(?<parens>\\((?<value>[^)]*)\\))?(?= |$)", case_sensitive)
97
+ m = title.match(rx)
98
+
99
+ if m.nil? && rename_to && force
100
+ title.tag!(rename_to, value: value, single: single)
101
+ elsif m
102
+ title.gsub!(rx) do
103
+ rename_to ? "@#{rename_to}#{value.nil? ? m['parens'] : "(#{value})"}" : ''
104
+ end
105
+
106
+ title.dedup_tags!
107
+ title.chomp!
108
+
109
+ if rename_to
110
+ f = "@#{tag}".cyan
111
+ t = "@#{rename_to}".cyan
112
+ Doing.logger.write(log_level, 'Tag:', %(renamed #{f} to #{t} in "#{title}"))
113
+ else
114
+ f = "@#{tag}".cyan
115
+ Doing.logger.write(log_level, 'Tag:', %(removed #{f} from "#{title}"))
116
+ end
117
+ else
118
+ Doing.logger.debug('Skipped:', "not tagged #{"@#{tag}".cyan}")
119
+ end
120
+ elsif title =~ /@#{tag}(?=[ (]|$)/
121
+ Doing.logger.debug('Skipped:', "already tagged #{"@#{tag}".cyan}")
122
+ return title
123
+ else
124
+ add = tag
125
+ add += "(#{value})" unless value.nil?
126
+ title.chomp!
127
+ title += " @#{add}"
128
+
129
+ title.dedup_tags!
130
+ title.chomp!
131
+ Doing.logger.write(log_level, 'Tag:', %(added #{('@' + tag).cyan} to "#{title}"))
132
+ end
133
+
134
+ title.gsub(/ +/, ' ')
135
+ end
136
+
137
+ ##
138
+ ## Remove duplicate tags, leaving only first occurrence
139
+ ##
140
+ ## @return Deduplicated string
141
+ ##
142
+ def dedup_tags
143
+ title = dup
144
+ tags = title.scan(/(?<=\A| )(@(\S+?)(\([^)]+\))?)(?= |\Z)/).uniq
145
+ tags.each do |tag|
146
+ found = false
147
+ title.gsub!(/( |^)#{tag[1]}(\([^)]+\))?(?= |$)/) do |m|
148
+ if found
149
+ ''
150
+ else
151
+ found = true
152
+ m
153
+ end
154
+ end
155
+ end
156
+ title
157
+ end
158
+
159
+ ## @see #dedup_tags
160
+ def dedup_tags!
161
+ replace dedup_tags
162
+ end
163
+ end
164
+ end
@@ -0,0 +1,168 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doing
4
+ ##
5
+ ## String helpers
6
+ ##
7
+ class ::String
8
+ # Compress multiple spaces to single space
9
+ def compress
10
+ gsub(/ +/, ' ').strip
11
+ end
12
+
13
+ def compress!
14
+ replace compress
15
+ end
16
+
17
+ def simple_wrap(width)
18
+ str = gsub(/@\S+\(.*?\)/) { |tag| tag.gsub(/\s/, '%%%%') }
19
+ words = str.split(/ /).map { |word| word.gsub(/%%%%/, ' ') }
20
+ out = []
21
+ line = []
22
+
23
+ words.each do |word|
24
+ if word.uncolor.length >= width
25
+ chars = word.uncolor.split('')
26
+ out << chars.slice!(0, width - 1).join('') while chars.count >= width
27
+ line << chars.join('')
28
+ next
29
+ elsif line.join(' ').uncolor.length + word.uncolor.length + 1 > width
30
+ out.push(line.join(' '))
31
+ line.clear
32
+ end
33
+
34
+ line << word.uncolor
35
+ end
36
+ out.push(line.join(' '))
37
+ out.join("\n")
38
+ end
39
+
40
+ ##
41
+ ## Wrap string at word breaks, respecting tags
42
+ ##
43
+ ## @param len [Integer] The length
44
+ ## @param offset [Integer] (Optional) The width to pad each subsequent line
45
+ ## @param prefix [String] (Optional) A prefix to add to each line
46
+ ##
47
+ def wrap(len, pad: 0, indent: ' ', offset: 0, prefix: '', color: '', after: '', reset: '', pad_first: false)
48
+ last_color = color.empty? ? '' : after.last_color
49
+ note_rx = /(?mi)(?<!\\)%(?<width>-?\d+)?(?:\^(?<mchar>.))?(?:(?<ichar>[ _t]|[^a-z0-9])(?<icount>\d+))?(?<prefix>.[ _t]?)?note/
50
+ note = ''
51
+ after = after.dup if after.frozen?
52
+ after.sub!(note_rx) do
53
+ note = Regexp.last_match(0)
54
+ ''
55
+ end
56
+
57
+ left_pad = ' ' * offset
58
+ left_pad += indent
59
+
60
+ # return "#{left_pad}#{prefix}#{color}#{self}#{last_color} #{note}" unless len.positive?
61
+
62
+ # Don't break inside of tag values
63
+ str = gsub(/@\S+\(.*?\)/) { |tag| tag.gsub(/\s/, '%%%%') }.gsub(/\n/, ' ')
64
+
65
+ words = str.split(/ /).map { |word| word.gsub(/%%%%/, ' ') }
66
+ out = []
67
+ line = []
68
+
69
+ words.each do |word|
70
+ if word.uncolor.length >= len
71
+ chars = word.uncolor.split('')
72
+ out << chars.slice!(0, len - 1).join('') while chars.count >= len
73
+ line << chars.join('')
74
+ next
75
+ elsif line.join(' ').uncolor.length + word.uncolor.length + 1 > len
76
+ out.push(line.join(' '))
77
+ line.clear
78
+ end
79
+
80
+ line << word.uncolor
81
+ end
82
+ out.push(line.join(' '))
83
+
84
+ last_color = ''
85
+ out[0] = format("%-#{pad}s%s%s", out[0], last_color, after)
86
+
87
+ out.map.with_index { |l, idx|
88
+ if !pad_first && idx == 0
89
+ "#{color}#{prefix}#{l}#{last_color}"
90
+ else
91
+ "#{left_pad}#{color}#{prefix}#{l}#{last_color}"
92
+ end
93
+ }.join("\n") + " #{note}".chomp
94
+ # res.join("\n").strip + last_color + " #{note}".chomp
95
+ end
96
+
97
+ ##
98
+ ## Capitalize on the first character on string
99
+ ##
100
+ ## @return Capitalized string
101
+ ##
102
+ def cap_first
103
+ sub(/^\w/) do |m|
104
+ m.upcase
105
+ end
106
+ end
107
+
108
+ ##
109
+ ## Pluralize a string based on quantity
110
+ ##
111
+ ## @param number [Integer] the quantity of the
112
+ ## object the string represents
113
+ ##
114
+ def to_p(number)
115
+ number == 1 ? self : "#{self}s"
116
+ end
117
+
118
+ ##
119
+ ## Convert a string value to an appropriate type. If
120
+ ## kind is not specified, '[one, two]' becomes an Array,
121
+ ## '1' becomes Integer, '1.5' becomes Float, 'true' or
122
+ ## 'yes' becomes TrueClass, 'false' or 'no' becomes
123
+ ## FalseClass.
124
+ ##
125
+ ## @param kind [String] specify string, array,
126
+ ## integer, float, symbol, or boolean
127
+ ## (falls back to string if value is
128
+ ## not recognized)
129
+ ## @return Converted object type
130
+ def set_type(kind = nil)
131
+ if kind
132
+ case kind.to_s
133
+ when /^a/i
134
+ gsub(/^\[ *| *\]$/, '').split(/ *, */)
135
+ when /^i/i
136
+ to_i
137
+ when /^(fa|tr)/i
138
+ to_bool
139
+ when /^f/i
140
+ to_f
141
+ when /^sy/i
142
+ sub(/^:/, '').to_sym
143
+ when /^b/i
144
+ self =~ /^(true|yes)$/ ? true : false
145
+ else
146
+ to_s
147
+ end
148
+ else
149
+ case self
150
+ when /(^\[.*?\]$| *, *)/
151
+ gsub(/^\[ *| *\]$/, '').split(/ *, */)
152
+ when /^[0-9]+$/
153
+ to_i
154
+ when /^[0-9]+\.[0-9]+$/
155
+ to_f
156
+ when /^:\w+/
157
+ sub(/^:/, '').to_sym
158
+ when /^(true|yes)$/i
159
+ true
160
+ when /^(false|no)$/i
161
+ false
162
+ else
163
+ to_s
164
+ end
165
+ end
166
+ end
167
+ end
168
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doing
4
+ ##
5
+ ## String truncation
6
+ ##
7
+ class ::String
8
+ ##
9
+ ## Truncate to nearest word
10
+ ##
11
+ ## @param len The length
12
+ ##
13
+ def trunc(len, ellipsis: '...')
14
+ return self if length <= len
15
+
16
+ total = 0
17
+ res = []
18
+
19
+ split(/ /).each do |word|
20
+ break if total + 1 + word.length > len
21
+
22
+ total += 1 + word.length
23
+ res.push(word)
24
+ end
25
+ res.join(' ') + ellipsis
26
+ end
27
+
28
+ def trunc!(len, ellipsis: '...')
29
+ replace trunc(len, ellipsis: ellipsis)
30
+ end
31
+
32
+ ##
33
+ ## Truncate from middle to end at nearest word
34
+ ##
35
+ ## @param len The length
36
+ ##
37
+ def truncend(len, ellipsis: '...')
38
+ return self if length <= len
39
+
40
+ total = 0
41
+ res = []
42
+
43
+ split(/ /).reverse.each do |word|
44
+ break if total + 1 + word.length > len
45
+
46
+ total += 1 + word.length
47
+ res.unshift(word)
48
+ end
49
+ ellipsis + res.join(' ')
50
+ end
51
+
52
+ def truncend!(len, ellipsis: '...')
53
+ replace truncend(len, ellipsis: ellipsis)
54
+ end
55
+
56
+ ##
57
+ ## Truncate string in the middle, separating at nearest word
58
+ ##
59
+ ## @param len The length
60
+ ## @param ellipsis The ellipsis
61
+ ##
62
+ def truncmiddle(len, ellipsis: '...')
63
+ return self if length <= len
64
+ len -= (ellipsis.length / 2).to_i
65
+ half = (len / 2).to_i
66
+ start = trunc(half, ellipsis: ellipsis)
67
+ finish = truncend(half, ellipsis: '')
68
+ start + finish
69
+ end
70
+
71
+ def truncmiddle!(len, ellipsis: '...')
72
+ replace truncmiddle(len, ellipsis: ellipsis)
73
+ end
74
+ end
75
+ end