doing 2.1.26 → 2.1.30

Sign up to get free protection for your applications and to get access to all the features.
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