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.
- checksums.yaml +4 -4
- data/.yardoc/checksums +15 -20
- data/.yardoc/object_types +0 -0
- data/.yardoc/objects/root.dat +0 -0
- data/CHANGELOG.md +322 -108
- data/Dockerfile +5 -5
- data/Dockerfile-2.6 +5 -5
- data/Dockerfile-2.7 +5 -4
- data/Dockerfile-3.0 +5 -4
- data/Gemfile.lock +1 -1
- data/README.md +1 -1
- data/Rakefile +2 -3
- data/bin/commands/add_section.rb +15 -0
- data/bin/commands/again.rb +57 -0
- data/bin/commands/archive.rb +55 -0
- data/bin/commands/cancel.rb +60 -0
- data/bin/commands/changes.rb +83 -0
- data/bin/commands/choose.rb +9 -0
- data/bin/commands/colors.rb +21 -0
- data/bin/commands/commands.rb +89 -0
- data/bin/commands/commands_accepting.rb +76 -0
- data/bin/commands/completion.rb +27 -0
- data/bin/commands/config.rb +245 -0
- data/bin/commands/done.rb +235 -0
- data/bin/commands/finish.rb +126 -0
- data/bin/commands/flag.rb +90 -0
- data/bin/commands/grep.rb +108 -0
- data/bin/commands/import.rb +71 -0
- data/bin/commands/install_fzf.rb +17 -0
- data/bin/commands/last.rb +81 -0
- data/bin/commands/meanwhile.rb +76 -0
- data/bin/commands/note.rb +91 -0
- data/bin/commands/now.rb +145 -0
- data/bin/commands/on.rb +65 -0
- data/bin/commands/open.rb +53 -0
- data/bin/commands/plugins.rb +23 -0
- data/bin/commands/recent.rb +77 -0
- data/bin/commands/redo.rb +26 -0
- data/bin/commands/reset.rb +73 -0
- data/bin/commands/rotate.rb +42 -0
- data/bin/commands/sections.rb +11 -0
- data/bin/commands/select.rb +105 -0
- data/bin/commands/show.rb +185 -0
- data/bin/commands/since.rb +63 -0
- data/bin/commands/tag.rb +149 -0
- data/bin/commands/tag_dir.rb +29 -0
- data/bin/commands/tags.rb +66 -0
- data/bin/commands/template.rb +61 -0
- data/bin/commands/today.rb +64 -0
- data/bin/commands/undo.rb +49 -0
- data/bin/commands/view.rb +201 -0
- data/bin/commands/views.rb +11 -0
- data/bin/commands/yesterday.rb +72 -0
- data/bin/doing +241 -3706
- data/docs/doc/Array.html +3 -502
- data/docs/doc/BooleanTermParser/Clause.html +1 -1
- data/docs/doc/BooleanTermParser/Operator.html +1 -1
- data/docs/doc/BooleanTermParser/Query.html +1 -1
- data/docs/doc/BooleanTermParser/QueryParser.html +1 -1
- data/docs/doc/BooleanTermParser/QueryTransformer.html +1 -1
- data/docs/doc/BooleanTermParser.html +1 -1
- data/docs/doc/Doing/Color.html +62 -56
- data/docs/doc/Doing/Completion.html +1 -1
- data/docs/doc/Doing/Configuration.html +36 -1
- data/docs/doc/Doing/Errors/DoingNoTraceError.html +1 -1
- data/docs/doc/Doing/Errors/DoingRuntimeError.html +1 -1
- data/docs/doc/Doing/Errors/DoingStandardError.html +1 -1
- data/docs/doc/Doing/Errors/EmptyInput.html +1 -1
- data/docs/doc/Doing/Errors/NoResults.html +1 -1
- data/docs/doc/Doing/Errors/PluginException.html +1 -1
- data/docs/doc/Doing/Errors/UserCancelled.html +1 -1
- data/docs/doc/Doing/Errors/WrongCommand.html +1 -1
- data/docs/doc/Doing/Errors.html +1 -1
- data/docs/doc/Doing/Hooks.html +1 -1
- data/docs/doc/Doing/Item.html +1 -1
- data/docs/doc/Doing/Items.html +2 -2
- data/docs/doc/Doing/LogAdapter.html +1 -1
- data/docs/doc/Doing/Note.html +2 -2
- data/docs/doc/Doing/Pager.html +1 -1
- data/docs/doc/Doing/Plugins.html +1 -1
- data/docs/doc/Doing/Prompt.html +46 -1
- data/docs/doc/Doing/Section.html +1 -1
- data/docs/doc/Doing/TemplateString.html +2 -2
- data/docs/doc/Doing/Types.html +41 -1
- data/docs/doc/Doing/Util/Backup.html +1 -1
- data/docs/doc/Doing/Util.html +1 -1
- data/docs/doc/Doing/WWID.html +10 -10
- data/docs/doc/Doing.html +3 -3
- data/docs/doc/FalseClass.html +235 -0
- data/docs/doc/GLI/Commands/Help.html +1 -1
- data/docs/doc/GLI/Commands/MarkdownDocumentListener.html +1 -1
- data/docs/doc/GLI/Commands.html +1 -1
- data/docs/doc/GLI.html +1 -1
- data/docs/doc/Hash.html +1 -1
- data/docs/doc/Numeric.html +1 -1
- data/docs/doc/Object.html +203 -0
- data/docs/doc/PhraseParser/Operator.html +1 -1
- data/docs/doc/PhraseParser/PhraseClause.html +1 -1
- data/docs/doc/PhraseParser/Query.html +1 -1
- data/docs/doc/PhraseParser/QueryParser.html +1 -1
- data/docs/doc/PhraseParser/QueryTransformer.html +1 -1
- data/docs/doc/PhraseParser/TermClause.html +1 -1
- data/docs/doc/PhraseParser.html +1 -1
- data/docs/doc/Status.html +1 -1
- data/docs/doc/String.html +287 -3155
- data/docs/doc/Symbol.html +40 -6
- data/docs/doc/Time.html +1 -1
- data/docs/doc/TrueClass.html +235 -0
- data/docs/doc/_index.html +5 -10
- data/docs/doc/class_list.html +1 -1
- data/docs/doc/file.README.html +2 -2
- data/docs/doc/index.html +2 -2
- data/docs/doc/method_list.html +289 -681
- data/docs/doc/top-level-namespace.html +2 -2
- data/doing.rdoc +306 -205
- data/lib/completion/_doing.zsh +35 -35
- data/lib/completion/doing.bash +30 -30
- data/lib/completion/doing.fish +88 -78
- data/lib/doing/array/array.rb +4 -0
- data/lib/doing/array/nested_hash.rb +17 -0
- data/lib/doing/{array.rb → array/tags.rb} +7 -25
- data/lib/doing/changelog/change.rb +26 -11
- data/lib/doing/changelog/changes.rb +31 -4
- data/lib/doing/{array_chronify.rb → chronify/array.rb} +0 -0
- data/lib/doing/chronify/chronify.rb +5 -0
- data/lib/doing/{numeric_chronify.rb → chronify/numeric.rb} +0 -0
- data/lib/doing/{string_chronify.rb → chronify/string.rb} +0 -0
- data/lib/doing/colors.rb +115 -54
- data/lib/doing/configuration.rb +5 -0
- data/lib/doing/good.rb +8 -0
- data/lib/doing/help_monkey_patch.rb +6 -5
- data/lib/doing/item.rb +5 -5
- data/lib/doing/items.rb +2 -2
- data/lib/doing/log_adapter.rb +35 -2
- data/lib/doing/normalize.rb +188 -0
- data/lib/doing/pager.rb +1 -0
- data/lib/doing/plugins/export/dayone_export.rb +1 -1
- data/lib/doing/plugins/export/html_export.rb +1 -1
- data/lib/doing/plugins/export/json_export.rb +1 -1
- data/lib/doing/plugins/export/markdown_export.rb +1 -1
- data/lib/doing/plugins/export/template_export.rb +3 -1
- data/lib/doing/prompt.rb +9 -3
- data/lib/doing/string/highlight.rb +95 -0
- data/lib/doing/string/query.rb +129 -0
- data/lib/doing/string/string.rb +12 -0
- data/lib/doing/string/tags.rb +164 -0
- data/lib/doing/string/transform.rb +168 -0
- data/lib/doing/string/truncate.rb +75 -0
- data/lib/doing/string/url.rb +82 -0
- data/lib/doing/template_string.rb +0 -22
- data/lib/doing/types.rb +8 -0
- data/lib/doing/util.rb +13 -9
- data/lib/doing/version.rb +1 -1
- data/lib/doing/wwid.rb +53 -35
- data/lib/doing.rb +4 -6
- data/lib/examples/commands/wiki.rb +6 -7
- data/lib/examples/plugins/wiki_export/wiki_export.rb +1 -1
- data/lib/helpers/threaded_tests.rb +39 -20
- data/scripts/deploy.rb +107 -0
- data/scripts/runtests.sh +4 -0
- metadata +63 -8
- data/lib/doing/string.rb +0 -765
- 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,
|
|
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
|
-
|
|
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,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
|