doing 2.1.26 → 2.1.30

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 (156) 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 +52 -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 +2 -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 +53 -12
  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 +8 -8
  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/open.rb +3 -3
  34. data/bin/commands/recent.rb +3 -4
  35. data/bin/commands/redo.rb +6 -2
  36. data/bin/commands/reset.rb +19 -52
  37. data/bin/commands/rotate.rb +5 -36
  38. data/bin/commands/select.rb +23 -41
  39. data/bin/commands/show.rb +28 -74
  40. data/bin/commands/since.rb +3 -4
  41. data/bin/commands/tag.rb +4 -34
  42. data/bin/commands/tags.rb +5 -32
  43. data/bin/commands/today.rb +3 -4
  44. data/bin/commands/view.rb +36 -73
  45. data/bin/commands/yesterday.rb +4 -5
  46. data/bin/doing +150 -13
  47. data/docs/doc/Array.html +3 -502
  48. data/docs/doc/BooleanTermParser/Clause.html +1 -1
  49. data/docs/doc/BooleanTermParser/Operator.html +1 -1
  50. data/docs/doc/BooleanTermParser/Query.html +1 -1
  51. data/docs/doc/BooleanTermParser/QueryParser.html +1 -1
  52. data/docs/doc/BooleanTermParser/QueryTransformer.html +1 -1
  53. data/docs/doc/BooleanTermParser.html +1 -1
  54. data/docs/doc/Doing/Color.html +62 -56
  55. data/docs/doc/Doing/Completion.html +1 -1
  56. data/docs/doc/Doing/Configuration.html +35 -1
  57. data/docs/doc/Doing/Errors/DoingNoTraceError.html +1 -1
  58. data/docs/doc/Doing/Errors/DoingRuntimeError.html +1 -1
  59. data/docs/doc/Doing/Errors/DoingStandardError.html +1 -1
  60. data/docs/doc/Doing/Errors/EmptyInput.html +1 -1
  61. data/docs/doc/Doing/Errors/NoResults.html +1 -1
  62. data/docs/doc/Doing/Errors/PluginException.html +1 -1
  63. data/docs/doc/Doing/Errors/UserCancelled.html +1 -1
  64. data/docs/doc/Doing/Errors/WrongCommand.html +1 -1
  65. data/docs/doc/Doing/Errors.html +1 -1
  66. data/docs/doc/Doing/Hooks.html +1 -1
  67. data/docs/doc/Doing/Item.html +1 -1
  68. data/docs/doc/Doing/Items.html +2 -2
  69. data/docs/doc/Doing/LogAdapter.html +1 -1
  70. data/docs/doc/Doing/Note.html +2 -2
  71. data/docs/doc/Doing/Pager.html +1 -1
  72. data/docs/doc/Doing/Plugins.html +1 -1
  73. data/docs/doc/Doing/Prompt.html +1 -1
  74. data/docs/doc/Doing/Section.html +1 -1
  75. data/docs/doc/Doing/TemplateString.html +2 -2
  76. data/docs/doc/Doing/Types.html +41 -1
  77. data/docs/doc/Doing/Util/Backup.html +1 -1
  78. data/docs/doc/Doing/Util.html +1 -1
  79. data/docs/doc/Doing/WWID.html +10 -10
  80. data/docs/doc/Doing.html +3 -3
  81. data/docs/doc/FalseClass.html +35 -1
  82. data/docs/doc/GLI/Commands/Help.html +1 -1
  83. data/docs/doc/GLI/Commands/MarkdownDocumentListener.html +1 -1
  84. data/docs/doc/GLI/Commands.html +1 -1
  85. data/docs/doc/GLI.html +1 -1
  86. data/docs/doc/Hash.html +1 -1
  87. data/docs/doc/Object.html +1 -1
  88. data/docs/doc/PhraseParser/Operator.html +1 -1
  89. data/docs/doc/PhraseParser/PhraseClause.html +1 -1
  90. data/docs/doc/PhraseParser/Query.html +1 -1
  91. data/docs/doc/PhraseParser/QueryParser.html +1 -1
  92. data/docs/doc/PhraseParser/QueryTransformer.html +1 -1
  93. data/docs/doc/PhraseParser/TermClause.html +1 -1
  94. data/docs/doc/PhraseParser.html +1 -1
  95. data/docs/doc/Status.html +1 -1
  96. data/docs/doc/String.html +287 -3155
  97. data/docs/doc/Symbol.html +40 -6
  98. data/docs/doc/Time.html +1 -1
  99. data/docs/doc/TrueClass.html +35 -1
  100. data/docs/doc/_index.html +5 -10
  101. data/docs/doc/class_list.html +1 -1
  102. data/docs/doc/file.README.html +2 -2
  103. data/docs/doc/index.html +2 -2
  104. data/docs/doc/method_list.html +278 -678
  105. data/docs/doc/top-level-namespace.html +2 -2
  106. data/doing.gemspec +1 -0
  107. data/doing.rdoc +297 -206
  108. data/lib/completion/_doing.zsh +32 -32
  109. data/lib/completion/doing.bash +30 -30
  110. data/lib/completion/doing.fish +87 -77
  111. data/lib/doing/array/array.rb +4 -0
  112. data/lib/doing/array/nested_hash.rb +17 -0
  113. data/lib/doing/{array.rb → array/tags.rb} +7 -25
  114. data/lib/doing/changelog/change.rb +26 -11
  115. data/lib/doing/changelog/changes.rb +37 -8
  116. data/lib/doing/changelog/version.rb +11 -3
  117. data/lib/doing/{array_chronify.rb → chronify/array.rb} +0 -0
  118. data/lib/doing/chronify/chronify.rb +5 -0
  119. data/lib/doing/{numeric_chronify.rb → chronify/numeric.rb} +0 -0
  120. data/lib/doing/{string_chronify.rb → chronify/string.rb} +0 -0
  121. data/lib/doing/colors.rb +115 -54
  122. data/lib/doing/completion/zsh_completion.rb +5 -0
  123. data/lib/doing/configuration.rb +9 -5
  124. data/lib/doing/good.rb +8 -0
  125. data/lib/doing/help_monkey_patch.rb +6 -5
  126. data/lib/doing/item.rb +5 -5
  127. data/lib/doing/items.rb +2 -2
  128. data/lib/doing/log_adapter.rb +35 -2
  129. data/lib/doing/normalize.rb +188 -0
  130. data/lib/doing/plugins/export/dayone_export.rb +1 -1
  131. data/lib/doing/plugins/export/html_export.rb +1 -1
  132. data/lib/doing/plugins/export/json_export.rb +1 -1
  133. data/lib/doing/plugins/export/markdown_export.rb +1 -1
  134. data/lib/doing/plugins/export/template_export.rb +3 -1
  135. data/lib/doing/prompt.rb +1 -3
  136. data/lib/doing/section.rb +1 -1
  137. data/lib/doing/string/highlight.rb +95 -0
  138. data/lib/doing/string/query.rb +129 -0
  139. data/lib/doing/string/string.rb +12 -0
  140. data/lib/doing/string/tags.rb +164 -0
  141. data/lib/doing/string/transform.rb +168 -0
  142. data/lib/doing/string/truncate.rb +75 -0
  143. data/lib/doing/string/url.rb +82 -0
  144. data/lib/doing/template_string.rb +0 -22
  145. data/lib/doing/types.rb +8 -0
  146. data/lib/doing/util.rb +13 -9
  147. data/lib/doing/version.rb +1 -1
  148. data/lib/doing/wwid.rb +54 -36
  149. data/lib/doing.rb +5 -6
  150. data/lib/examples/plugins/wiki_export/wiki_export.rb +1 -1
  151. data/lib/helpers/threaded_tests.rb +15 -2
  152. data/scripts/deploy.rb +107 -0
  153. data/scripts/runtests.sh +4 -0
  154. metadata +39 -8
  155. data/lib/doing/string.rb +0 -765
  156. data/lib/doing/symbol.rb +0 -28
@@ -0,0 +1,188 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doing
4
+ ##
5
+ ## String to symbol conversion
6
+ ##
7
+ class ::String
8
+ ##
9
+ ## Convert tag sort string to a qualified type
10
+ ##
11
+ ## @return [Symbol] :name or :time
12
+ ##
13
+ def normalize_tag_sort(default = :name)
14
+ case self
15
+ when /^n/i
16
+ :name
17
+ when /^t/i
18
+ :time
19
+ else
20
+ default
21
+ end
22
+ end
23
+
24
+ ## @see #normalize_tag_sort
25
+ def normalize_tag_sort!(default = :name)
26
+ replace normalize_tag_sort(default)
27
+ end
28
+
29
+ ##
30
+ ## Convert an age string to a qualified type
31
+ ##
32
+ ## @return [Symbol] :oldest or :newest
33
+ ##
34
+ def normalize_age(default = :newest)
35
+ case self
36
+ when /^o/i
37
+ :oldest
38
+ when /^n/i
39
+ :newest
40
+ else
41
+ default
42
+ end
43
+ end
44
+
45
+ ## @see #normalize_age
46
+ def normalize_age!(default = :newest)
47
+ replace normalize_age(default)
48
+ end
49
+
50
+ ##
51
+ ## Convert a sort order string to a qualified type
52
+ ##
53
+ ## @return [Symbol] :asc or :desc
54
+ ##
55
+ def normalize_order!(default = :asc)
56
+ replace normalize_order(default)
57
+ end
58
+
59
+ def normalize_order(default = :asc)
60
+ case self
61
+ when /^a/i
62
+ :asc
63
+ when /^d/i
64
+ :desc
65
+ else
66
+ default
67
+ end
68
+ end
69
+
70
+ ##
71
+ ## Convert a case sensitivity string to a symbol
72
+ ##
73
+ ## @return Symbol :smart, :sensitive, :ignore
74
+ ##
75
+ def normalize_case(default = :smart)
76
+ case self
77
+ when /^(c|sens)/i
78
+ :sensitive
79
+ when /^i/i
80
+ :ignore
81
+ when /^s/i
82
+ :smart
83
+ else
84
+ default.is_a?(Symbol) ? default : default.normalize_case
85
+ end
86
+ end
87
+
88
+ ## @see #normalize_case
89
+ def normalize_case!(default = :smart)
90
+ replace normalize_case(default)
91
+ end
92
+
93
+ ##
94
+ ## Convert a boolean string to a symbol
95
+ ##
96
+ ## @return Symbol :and, :or, or :not
97
+ ##
98
+ def normalize_bool(default = :and)
99
+ case self
100
+ when /(and|all)/i
101
+ :and
102
+ when /(any|or)/i
103
+ :or
104
+ when /(not|none)/i
105
+ :not
106
+ when /^p/i
107
+ :pattern
108
+ else
109
+ default.is_a?(Symbol) ? default : default.normalize_bool
110
+ end
111
+ end
112
+
113
+ ## @see #normalize_bool
114
+ def normalize_bool!(default = :and)
115
+ replace normalize_bool(default)
116
+ end
117
+
118
+ ##
119
+ ## Convert a matching configuration string to a symbol
120
+ ##
121
+ ## @param default [Symbol] the default matching
122
+ ## type to return if the string
123
+ ## doesn't match a known symbol
124
+ ## @return Symbol :fuzzy, :pattern, :exact
125
+ ##
126
+ def normalize_matching(default = :pattern)
127
+ case self
128
+ when /^f/i
129
+ :fuzzy
130
+ when /^p/i
131
+ :pattern
132
+ when /^e/i
133
+ :exact
134
+ else
135
+ default.is_a?(Symbol) ? default : default.normalize_matching
136
+ end
137
+ end
138
+
139
+ ## @see #normalize_matching
140
+ def normalize_matching!(default = :pattern)
141
+ replace normalize_bool(default)
142
+ end
143
+
144
+ ##
145
+ ## Adds ?: to any parentheticals in a regular expression
146
+ ## to avoid match groups
147
+ ##
148
+ ## @return [String] modified regular expression
149
+ ##
150
+ def normalize_trigger
151
+ gsub(/\((?!\?:)/, '(?:').downcase
152
+ end
153
+
154
+ ## @see #normalize_trigger
155
+ def normalize_trigger!
156
+ replace normalize_trigger
157
+ end
158
+ end
159
+
160
+ ##
161
+ ## Symbol helpers
162
+ ##
163
+ class ::Symbol
164
+ def normalize_tag_sort(default = :name)
165
+ to_s.normalize_tag_sort
166
+ end
167
+
168
+ def normalize_bool(default = :and)
169
+ to_s.normalize_bool(default)
170
+ end
171
+
172
+ def normalize_age(default = :newest)
173
+ to_s.normalize_age(default)
174
+ end
175
+
176
+ def normalize_order(default = :asc)
177
+ to_s.normalize_order(default)
178
+ end
179
+
180
+ def normalize_case(default = :smart)
181
+ to_s.normalize_case(default)
182
+ end
183
+
184
+ def normalize_matching(default = :pattern)
185
+ to_s.normalize_matching(default)
186
+ end
187
+ end
188
+ end
@@ -129,7 +129,7 @@ module Doing
129
129
  self.template('dayone')
130
130
  end
131
131
 
132
- totals = opt[:totals] ? wwid.tag_times(format: :markdown, sort_by_name: opt[:sort_tags], sort_order: opt[:tag_order]) : ''
132
+ totals = opt[:totals] ? wwid.tag_times(format: :markdown, sort_by: opt[:sort_tags], sort_order: opt[:tag_order]) : ''
133
133
 
134
134
  case digest
135
135
  when :day
@@ -70,7 +70,7 @@ module Doing
70
70
  self.template('css')
71
71
  end
72
72
 
73
- totals = opt[:totals] ? wwid.tag_times(format: :html, sort_by_name: opt[:sort_tags], sort_order: opt[:tag_order]) : ''
73
+ totals = opt[:totals] ? wwid.tag_times(format: :html, sort_by: opt[:sort_tags], sort_order: opt[:tag_order]) : ''
74
74
  engine = Haml::Engine.new(template)
75
75
  Doing.logger.debug('HTML Export:', "#{items_out.count} items output to HTML")
76
76
  @out = engine.render(Object.new,
@@ -91,7 +91,7 @@ module Doing
91
91
  JSON.pretty_generate({
92
92
  'section' => variables[:page_title],
93
93
  'items' => items_out,
94
- 'timers' => wwid.tag_times(format: :json, sort_by_name: opt[:sort_tags], sort_order: opt[:tag_order])
94
+ 'timers' => wwid.tag_times(format: :json, sort_by: opt[:sort_tags], sort_order: opt[:tag_order])
95
95
  })
96
96
  elsif opt[:output] == 'timeline'
97
97
  template = <<~EOTEMPLATE
@@ -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
@@ -107,9 +107,7 @@ module Doing
107
107
  ## @return (Bool) yes or no
108
108
  ##
109
109
  def yn(question, default_response: false)
110
- unless @force_answer.nil?
111
- return @force_answer
112
- end
110
+ return @force_answer == :yes ? true : false unless @force_answer.nil?
113
111
 
114
112
  $stdin.reopen('/dev/tty')
115
113
 
data/lib/doing/section.rb CHANGED
@@ -13,7 +13,7 @@ module Doing
13
13
  @original = if original.nil?
14
14
  "#{title}:"
15
15
  else
16
- original =~ /:(\s+@\S+(\(.*?\))?)*$/ ? original : "#{original}:"
16
+ original =~ /:(\s+@[^ (]+(\([^)]*\))?)*?$/ ? original : "#{original}:"
17
17
  end
18
18
  end
19
19
 
@@ -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