doing 2.1.23 → 2.1.27
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.yardoc/checksums +17 -21
- data/.yardoc/object_types +0 -0
- data/.yardoc/objects/root.dat +0 -0
- data/CHANGELOG.md +329 -102
- 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 +3 -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 +69 -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 -3662
- data/docs/doc/Array.html +13 -449
- data/docs/doc/BooleanTermParser/Clause.html +5 -5
- data/docs/doc/BooleanTermParser/Operator.html +4 -4
- data/docs/doc/BooleanTermParser/Query.html +8 -8
- data/docs/doc/BooleanTermParser/QueryParser.html +2 -2
- data/docs/doc/BooleanTermParser/QueryTransformer.html +2 -2
- data/docs/doc/BooleanTermParser.html +1 -1
- data/docs/doc/Doing/Color.html +65 -59
- data/docs/doc/Doing/Completion.html +2 -2
- data/docs/doc/Doing/Configuration.html +49 -16
- data/docs/doc/Doing/Errors/DoingNoTraceError.html +2 -2
- data/docs/doc/Doing/Errors/DoingRuntimeError.html +2 -2
- data/docs/doc/Doing/Errors/DoingStandardError.html +2 -2
- data/docs/doc/Doing/Errors/EmptyInput.html +2 -2
- data/docs/doc/Doing/Errors/NoResults.html +2 -2
- data/docs/doc/Doing/Errors/PluginException.html +3 -3
- data/docs/doc/Doing/Errors/UserCancelled.html +2 -2
- data/docs/doc/Doing/Errors/WrongCommand.html +2 -2
- data/docs/doc/Doing/Errors.html +1 -1
- data/docs/doc/Doing/Hooks.html +6 -6
- data/docs/doc/Doing/Item.html +50 -16
- data/docs/doc/Doing/Items.html +10 -10
- data/docs/doc/Doing/LogAdapter.html +24 -24
- data/docs/doc/Doing/Note.html +7 -7
- data/docs/doc/Doing/Pager.html +4 -4
- data/docs/doc/Doing/Plugins.html +7 -7
- data/docs/doc/Doing/Prompt.html +59 -14
- data/docs/doc/Doing/Section.html +6 -6
- data/docs/doc/Doing/TemplateString.html +8 -8
- data/docs/doc/Doing/Types.html +46 -1
- data/docs/doc/Doing/Util/Backup.html +10 -10
- data/docs/doc/Doing/Util.html +15 -15
- data/docs/doc/Doing/WWID.html +73 -61
- data/docs/doc/Doing.html +3 -3
- data/docs/doc/FalseClass.html +235 -0
- data/docs/doc/GLI/Commands/Help.html +3 -3
- data/docs/doc/GLI/Commands/MarkdownDocumentListener.html +17 -17
- data/docs/doc/GLI/Commands.html +1 -1
- data/docs/doc/GLI.html +1 -1
- data/docs/doc/Hash.html +45 -11
- data/docs/doc/Numeric.html +5 -5
- data/docs/doc/Object.html +203 -0
- data/docs/doc/PhraseParser/Operator.html +4 -4
- data/docs/doc/PhraseParser/PhraseClause.html +5 -5
- data/docs/doc/PhraseParser/Query.html +10 -10
- data/docs/doc/PhraseParser/QueryParser.html +2 -2
- data/docs/doc/PhraseParser/QueryTransformer.html +2 -2
- data/docs/doc/PhraseParser/TermClause.html +5 -5
- data/docs/doc/PhraseParser.html +1 -1
- data/docs/doc/Status.html +7 -7
- data/docs/doc/String.html +306 -3111
- data/docs/doc/Symbol.html +45 -11
- data/docs/doc/Time.html +6 -6
- data/docs/doc/TrueClass.html +235 -0
- data/docs/doc/_index.html +37 -19
- 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 +240 -576
- data/docs/doc/top-level-namespace.html +2 -2
- data/doing.rdoc +289 -169
- data/example_plugin.rb +2 -2
- data/lib/completion/_doing.zsh +35 -31
- data/lib/completion/doing.bash +30 -19
- data/lib/completion/doing.fish +81 -67
- 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 +14 -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/completion/fish_completion.rb +2 -1
- data/lib/doing/configuration.rb +9 -6
- data/lib/doing/good.rb +72 -0
- data/lib/doing/hash.rb +4 -0
- data/lib/doing/help_monkey_patch.rb +6 -5
- data/lib/doing/hooks.rb +3 -3
- data/lib/doing/item.rb +19 -15
- 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/plugins/import/calendar_import.rb +1 -1
- data/lib/doing/plugins/import/doing_import.rb +1 -1
- data/lib/doing/plugins/import/timing_import.rb +1 -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 +2 -24
- data/lib/doing/types.rb +9 -0
- data/lib/doing/util.rb +20 -16
- data/lib/doing/version.rb +1 -1
- data/lib/doing/wwid.rb +91 -51
- data/lib/doing.rb +5 -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 +69 -79
- data/lib/helpers/threaded_tests_string.rb +50 -0
- data/scripts/deploy.rb +107 -0
- data/scripts/runtests.sh +4 -0
- metadata +65 -8
- data/lib/doing/string.rb +0 -765
- data/lib/doing/symbol.rb +0 -28
@@ -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
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Doing
|
4
|
+
##
|
5
|
+
## URL linking and formatting
|
6
|
+
##
|
7
|
+
class ::String
|
8
|
+
##
|
9
|
+
## Turn raw urls into HTML links
|
10
|
+
##
|
11
|
+
## @param opt [Hash] Additional Options
|
12
|
+
##
|
13
|
+
## @option opt [Symbol] :format can be :markdown or
|
14
|
+
## :html (default)
|
15
|
+
##
|
16
|
+
def link_urls(**opt)
|
17
|
+
fmt = opt.fetch(:format, :html)
|
18
|
+
return self unless fmt
|
19
|
+
|
20
|
+
str = dup
|
21
|
+
|
22
|
+
str = str.remove_self_links if fmt == :markdown
|
23
|
+
|
24
|
+
str.replace_qualified_urls(format: fmt).clean_unlinked_urls
|
25
|
+
end
|
26
|
+
|
27
|
+
## @see #link_urls
|
28
|
+
def link_urls!(**opt)
|
29
|
+
fmt = opt.fetch(:format, :html)
|
30
|
+
replace link_urls(format: fmt)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Remove <self-linked> formatting
|
34
|
+
def remove_self_links
|
35
|
+
gsub(/<(.*?)>/) do |match|
|
36
|
+
m = Regexp.last_match
|
37
|
+
if m[1] =~ /^https?:/
|
38
|
+
m[1]
|
39
|
+
else
|
40
|
+
match
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Replace qualified urls
|
46
|
+
def replace_qualified_urls(**options)
|
47
|
+
fmt = options.fetch(:format, :html)
|
48
|
+
gsub(%r{(?mi)(?x:
|
49
|
+
(?<!["'\[(\\])
|
50
|
+
(?<protocol>(?:http|https)://)
|
51
|
+
(?<domain>[\w\-]+(?:\.[\w\-]+)+)
|
52
|
+
(?<path>[\w\-.,@?^=%&;:/~+#]*[\w\-@^=%&;/~+#])?
|
53
|
+
)}) do |_match|
|
54
|
+
m = Regexp.last_match
|
55
|
+
url = "#{m['domain']}#{m['path']}"
|
56
|
+
proto = m['protocol'].nil? ? 'http://' : m['protocol']
|
57
|
+
case fmt
|
58
|
+
when :terminal
|
59
|
+
TTY::Link.link_to("#{proto}#{url}", "#{proto}#{url}")
|
60
|
+
when :html
|
61
|
+
%(<a href="#{proto}#{url}" title="Link to #{m['domain']}">[#{url}]</a>)
|
62
|
+
when :markdown
|
63
|
+
"[#{url}](#{proto}#{url})"
|
64
|
+
else
|
65
|
+
m[0]
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Clean up unlinked <urls>
|
71
|
+
def clean_unlinked_urls
|
72
|
+
gsub(/<(\w+:.*?)>/) do |match|
|
73
|
+
m = Regexp.last_match
|
74
|
+
if m[1] =~ /<a href/
|
75
|
+
match
|
76
|
+
else
|
77
|
+
%(<a href="#{m[1]}" title="Link to #{m[1]}">[link]</a>)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -5,28 +5,6 @@ module Doing
|
|
5
5
|
## Template string formatting
|
6
6
|
##
|
7
7
|
class TemplateString < String
|
8
|
-
class ::String
|
9
|
-
##
|
10
|
-
## Extract the longest valid color from a string.
|
11
|
-
##
|
12
|
-
## Allows %colors to bleed into other text and still
|
13
|
-
## be recognized, e.g. %greensomething still finds
|
14
|
-
## %green.
|
15
|
-
##
|
16
|
-
## @return [String] a valid color name
|
17
|
-
## @api private
|
18
|
-
def validate_color
|
19
|
-
valid_color = nil
|
20
|
-
compiled = ''
|
21
|
-
split('').each do |char|
|
22
|
-
compiled += char
|
23
|
-
valid_color = compiled if Color.attributes.include?(compiled.to_sym)
|
24
|
-
end
|
25
|
-
|
26
|
-
valid_color
|
27
|
-
end
|
28
|
-
end
|
29
|
-
|
30
8
|
attr_reader :original
|
31
9
|
|
32
10
|
include Color
|
@@ -131,7 +109,7 @@ module Doing
|
|
131
109
|
|
132
110
|
after = m['after']
|
133
111
|
|
134
|
-
if value.
|
112
|
+
if !value.good?
|
135
113
|
after
|
136
114
|
else
|
137
115
|
pad = m['width'].to_i
|
@@ -183,7 +161,7 @@ module Doing
|
|
183
161
|
' '
|
184
162
|
else
|
185
163
|
line = l.gsub(/%/, '\%').strip.wrap(width, pad: pad, indent: indent, offset: 0, prefix: prefix, color: last_color, after: after, reset: reset, pad_first: true)
|
186
|
-
line.highlight_tags!(tags_color, last_color: last_color) unless !tags_color || tags_color.
|
164
|
+
line.highlight_tags!(tags_color, last_color: last_color) unless !tags_color || !tags_color.good?
|
187
165
|
"#{line} "
|
188
166
|
end
|
189
167
|
end.join("\n")
|
data/lib/doing/types.rb
CHANGED
@@ -2,6 +2,8 @@
|
|
2
2
|
|
3
3
|
module Doing
|
4
4
|
module Types
|
5
|
+
REGEX_CASE = /^[cis].*?$/i.freeze
|
6
|
+
REGEX_TAG_SORT = /^(?:n(?:ame)?|t(?:ime)?)$/i.freeze
|
5
7
|
REGEX_BOOL = /^(?:and|all|any|or|not|none|p(?:at(?:tern)?)?)$/i.freeze
|
6
8
|
REGEX_SORT_ORDER = /^(?:a(?:sc)?|d(?:esc)?)$/i.freeze
|
7
9
|
REGEX_VALUE_QUERY = /^(?:!)?@?(?:\S+) +(?:!?[<>=][=*]?|[$*^]=) +(?:.*?)$/.freeze
|
@@ -20,6 +22,13 @@ module Doing
|
|
20
22
|
DateBeginString = Class.new(DateTime)
|
21
23
|
DateEndString = Class.new(DateTime)
|
22
24
|
DateRangeString = Class.new(Array)
|
25
|
+
DateRangeOptionalString = Class.new(Array)
|
23
26
|
DateIntervalString = Class.new(DateTime)
|
27
|
+
BooleanSymbol = Class.new(Symbol)
|
28
|
+
CaseSymbol = Class.new(Symbol)
|
29
|
+
AgeSymbol = Class.new(String)
|
30
|
+
OrderSymbol = Class.new(Symbol)
|
31
|
+
TagSortSymbol = Class.new(Symbol)
|
32
|
+
MatchingSymbol = Class.new(Symbol)
|
24
33
|
end
|
25
34
|
end
|
data/lib/doing/util.rb
CHANGED
@@ -19,7 +19,7 @@ module Doing
|
|
19
19
|
## @param cli [String] The name or path of the cli
|
20
20
|
##
|
21
21
|
def exec_available(cli)
|
22
|
-
return false
|
22
|
+
return false unless cli.good?
|
23
23
|
|
24
24
|
!TTY::Which.which(cli).nil?
|
25
25
|
end
|
@@ -32,7 +32,7 @@ module Doing
|
|
32
32
|
##
|
33
33
|
def first_available_exec(*commands)
|
34
34
|
commands.compact.map(&:strip).reject(&:empty?).uniq
|
35
|
-
|
35
|
+
.find { |cmd| exec_available(cmd.split.first) }
|
36
36
|
end
|
37
37
|
|
38
38
|
def merge_default_proc(target, overwrite)
|
@@ -56,7 +56,7 @@ module Doing
|
|
56
56
|
# @param [Hash] other_hash The other hash
|
57
57
|
#
|
58
58
|
def deep_merge_hashes(master_hash, other_hash)
|
59
|
-
deep_merge_hashes!(master_hash.
|
59
|
+
deep_merge_hashes!(master_hash.clone, other_hash)
|
60
60
|
end
|
61
61
|
|
62
62
|
# Merges a master hash with another hash, recursively.
|
@@ -157,42 +157,46 @@ module Doing
|
|
157
157
|
def find_default_editor(editor_for = 'default')
|
158
158
|
# return nil unless $stdout.isatty || ENV['DOING_EDITOR_TEST']
|
159
159
|
|
160
|
-
if ENV['DOING_EDITOR_TEST']
|
161
|
-
return ENV['EDITOR']
|
162
|
-
end
|
160
|
+
return ENV['EDITOR'] if ENV['DOING_EDITOR_TEST']
|
163
161
|
|
164
162
|
editor_config = Doing.config.settings['editors']
|
165
163
|
|
166
164
|
if editor_config.is_a?(String)
|
167
|
-
|
165
|
+
msg = "Please update your configuration, 'editors' should be a mapping."
|
166
|
+
msg << ' Delete the key and run `doing config --update`.'
|
167
|
+
Doing.logger.warn('Deprecated:', msg)
|
168
168
|
return editor_config
|
169
169
|
end
|
170
170
|
|
171
171
|
if editor_config[editor_for]
|
172
172
|
editor = editor_config[editor_for]
|
173
|
-
|
174
|
-
return editor
|
173
|
+
Doing.logger.debug('Editor:', "Using #{editor} from config 'editors.#{editor_for}' for #{editor_for}")
|
174
|
+
return editor if editor.good?
|
175
175
|
end
|
176
176
|
|
177
177
|
if editor_for != 'editor' && editor_config['default']
|
178
178
|
editor = editor_config['default']
|
179
|
-
|
180
|
-
return editor
|
179
|
+
Doing.logger.debug('Editor:', "Using #{editor} from config: 'editors.default' for #{editor_for}")
|
180
|
+
return editor if editor.good?
|
181
181
|
end
|
182
182
|
|
183
183
|
editor ||= ENV['DOING_EDITOR'] || ENV['GIT_EDITOR'] || ENV['EDITOR']
|
184
184
|
|
185
|
-
|
186
|
-
|
185
|
+
if editor.good?
|
186
|
+
Doing.logger.debug('Editor:', "Found editor in environment variables: #{editor} for #{editor_for}")
|
187
187
|
return editor
|
188
188
|
end
|
189
189
|
|
190
|
-
Doing.logger.debug('
|
190
|
+
Doing.logger.debug('Editor:', 'No EDITOR environment variable, testing available editors')
|
191
191
|
editors = %w[vim vi code subl mate mvim nano emacs]
|
192
192
|
editors.each do |ed|
|
193
|
-
|
193
|
+
try = TTY::Which.which(ed)
|
194
|
+
if try
|
195
|
+
Doing.logger.debug('Editor:', "Using editor #{try} for #{editor_for}")
|
196
|
+
return try
|
197
|
+
end
|
194
198
|
|
195
|
-
Doing.logger.debug('
|
199
|
+
Doing.logger.debug('Editor:', "#{ed} not available")
|
196
200
|
end
|
197
201
|
|
198
202
|
nil
|
data/lib/doing/version.rb
CHANGED