doing 2.1.24 → 2.1.28

Sign up to get free protection for your applications and to get access to all the features.
Files changed (170) hide show
  1. checksums.yaml +4 -4
  2. data/.yardoc/checksums +17 -21
  3. data/.yardoc/object_types +0 -0
  4. data/.yardoc/objects/root.dat +0 -0
  5. data/CHANGELOG.md +325 -102
  6. data/Dockerfile +5 -5
  7. data/Dockerfile-2.6 +5 -5
  8. data/Dockerfile-2.7 +5 -4
  9. data/Dockerfile-3.0 +5 -4
  10. data/Gemfile.lock +1 -1
  11. data/README.md +1 -1
  12. data/Rakefile +3 -3
  13. data/bin/commands/add_section.rb +15 -0
  14. data/bin/commands/again.rb +57 -0
  15. data/bin/commands/archive.rb +55 -0
  16. data/bin/commands/cancel.rb +60 -0
  17. data/bin/commands/changes.rb +73 -0
  18. data/bin/commands/choose.rb +9 -0
  19. data/bin/commands/colors.rb +21 -0
  20. data/bin/commands/commands.rb +89 -0
  21. data/bin/commands/commands_accepting.rb +76 -0
  22. data/bin/commands/completion.rb +27 -0
  23. data/bin/commands/config.rb +245 -0
  24. data/bin/commands/done.rb +235 -0
  25. data/bin/commands/finish.rb +126 -0
  26. data/bin/commands/flag.rb +90 -0
  27. data/bin/commands/grep.rb +108 -0
  28. data/bin/commands/import.rb +71 -0
  29. data/bin/commands/install_fzf.rb +17 -0
  30. data/bin/commands/last.rb +81 -0
  31. data/bin/commands/meanwhile.rb +76 -0
  32. data/bin/commands/note.rb +91 -0
  33. data/bin/commands/now.rb +145 -0
  34. data/bin/commands/on.rb +65 -0
  35. data/bin/commands/open.rb +53 -0
  36. data/bin/commands/plugins.rb +23 -0
  37. data/bin/commands/recent.rb +77 -0
  38. data/bin/commands/redo.rb +26 -0
  39. data/bin/commands/reset.rb +73 -0
  40. data/bin/commands/rotate.rb +42 -0
  41. data/bin/commands/sections.rb +11 -0
  42. data/bin/commands/select.rb +105 -0
  43. data/bin/commands/show.rb +185 -0
  44. data/bin/commands/since.rb +63 -0
  45. data/bin/commands/tag.rb +149 -0
  46. data/bin/commands/tag_dir.rb +29 -0
  47. data/bin/commands/tags.rb +66 -0
  48. data/bin/commands/template.rb +61 -0
  49. data/bin/commands/today.rb +64 -0
  50. data/bin/commands/undo.rb +49 -0
  51. data/bin/commands/view.rb +201 -0
  52. data/bin/commands/views.rb +11 -0
  53. data/bin/commands/yesterday.rb +72 -0
  54. data/bin/doing +241 -3662
  55. data/docs/doc/Array.html +13 -449
  56. data/docs/doc/BooleanTermParser/Clause.html +5 -5
  57. data/docs/doc/BooleanTermParser/Operator.html +4 -4
  58. data/docs/doc/BooleanTermParser/Query.html +8 -8
  59. data/docs/doc/BooleanTermParser/QueryParser.html +2 -2
  60. data/docs/doc/BooleanTermParser/QueryTransformer.html +2 -2
  61. data/docs/doc/BooleanTermParser.html +1 -1
  62. data/docs/doc/Doing/Color.html +65 -59
  63. data/docs/doc/Doing/Completion.html +2 -2
  64. data/docs/doc/Doing/Configuration.html +49 -16
  65. data/docs/doc/Doing/Errors/DoingNoTraceError.html +2 -2
  66. data/docs/doc/Doing/Errors/DoingRuntimeError.html +2 -2
  67. data/docs/doc/Doing/Errors/DoingStandardError.html +2 -2
  68. data/docs/doc/Doing/Errors/EmptyInput.html +2 -2
  69. data/docs/doc/Doing/Errors/NoResults.html +2 -2
  70. data/docs/doc/Doing/Errors/PluginException.html +3 -3
  71. data/docs/doc/Doing/Errors/UserCancelled.html +2 -2
  72. data/docs/doc/Doing/Errors/WrongCommand.html +2 -2
  73. data/docs/doc/Doing/Errors.html +1 -1
  74. data/docs/doc/Doing/Hooks.html +6 -6
  75. data/docs/doc/Doing/Item.html +50 -16
  76. data/docs/doc/Doing/Items.html +10 -10
  77. data/docs/doc/Doing/LogAdapter.html +24 -24
  78. data/docs/doc/Doing/Note.html +7 -7
  79. data/docs/doc/Doing/Pager.html +4 -4
  80. data/docs/doc/Doing/Plugins.html +7 -7
  81. data/docs/doc/Doing/Prompt.html +59 -14
  82. data/docs/doc/Doing/Section.html +6 -6
  83. data/docs/doc/Doing/TemplateString.html +8 -8
  84. data/docs/doc/Doing/Types.html +46 -1
  85. data/docs/doc/Doing/Util/Backup.html +10 -10
  86. data/docs/doc/Doing/Util.html +15 -15
  87. data/docs/doc/Doing/WWID.html +73 -61
  88. data/docs/doc/Doing.html +3 -3
  89. data/docs/doc/FalseClass.html +235 -0
  90. data/docs/doc/GLI/Commands/Help.html +3 -3
  91. data/docs/doc/GLI/Commands/MarkdownDocumentListener.html +17 -17
  92. data/docs/doc/GLI/Commands.html +1 -1
  93. data/docs/doc/GLI.html +1 -1
  94. data/docs/doc/Hash.html +45 -11
  95. data/docs/doc/Numeric.html +5 -5
  96. data/docs/doc/Object.html +203 -0
  97. data/docs/doc/PhraseParser/Operator.html +4 -4
  98. data/docs/doc/PhraseParser/PhraseClause.html +5 -5
  99. data/docs/doc/PhraseParser/Query.html +10 -10
  100. data/docs/doc/PhraseParser/QueryParser.html +2 -2
  101. data/docs/doc/PhraseParser/QueryTransformer.html +2 -2
  102. data/docs/doc/PhraseParser/TermClause.html +5 -5
  103. data/docs/doc/PhraseParser.html +1 -1
  104. data/docs/doc/Status.html +7 -7
  105. data/docs/doc/String.html +306 -3111
  106. data/docs/doc/Symbol.html +45 -11
  107. data/docs/doc/Time.html +6 -6
  108. data/docs/doc/TrueClass.html +235 -0
  109. data/docs/doc/_index.html +37 -19
  110. data/docs/doc/class_list.html +1 -1
  111. data/docs/doc/file.README.html +2 -2
  112. data/docs/doc/index.html +2 -2
  113. data/docs/doc/method_list.html +240 -576
  114. data/docs/doc/top-level-namespace.html +2 -2
  115. data/doing.rdoc +297 -169
  116. data/example_plugin.rb +2 -2
  117. data/lib/completion/_doing.zsh +35 -31
  118. data/lib/completion/doing.bash +30 -19
  119. data/lib/completion/doing.fish +81 -67
  120. data/lib/doing/array/array.rb +4 -0
  121. data/lib/doing/array/nested_hash.rb +17 -0
  122. data/lib/doing/{array.rb → array/tags.rb} +7 -25
  123. data/lib/doing/changelog/change.rb +26 -11
  124. data/lib/doing/changelog/changes.rb +16 -4
  125. data/lib/doing/{array_chronify.rb → chronify/array.rb} +0 -0
  126. data/lib/doing/chronify/chronify.rb +5 -0
  127. data/lib/doing/{numeric_chronify.rb → chronify/numeric.rb} +0 -0
  128. data/lib/doing/{string_chronify.rb → chronify/string.rb} +0 -0
  129. data/lib/doing/colors.rb +115 -54
  130. data/lib/doing/configuration.rb +9 -6
  131. data/lib/doing/good.rb +72 -0
  132. data/lib/doing/hash.rb +4 -0
  133. data/lib/doing/help_monkey_patch.rb +6 -5
  134. data/lib/doing/hooks.rb +3 -3
  135. data/lib/doing/item.rb +19 -15
  136. data/lib/doing/items.rb +2 -2
  137. data/lib/doing/log_adapter.rb +35 -2
  138. data/lib/doing/normalize.rb +188 -0
  139. data/lib/doing/pager.rb +1 -0
  140. data/lib/doing/plugins/export/dayone_export.rb +1 -1
  141. data/lib/doing/plugins/export/html_export.rb +1 -1
  142. data/lib/doing/plugins/export/json_export.rb +1 -1
  143. data/lib/doing/plugins/export/markdown_export.rb +1 -1
  144. data/lib/doing/plugins/export/template_export.rb +3 -1
  145. data/lib/doing/plugins/import/calendar_import.rb +1 -1
  146. data/lib/doing/plugins/import/doing_import.rb +1 -1
  147. data/lib/doing/plugins/import/timing_import.rb +1 -1
  148. data/lib/doing/prompt.rb +9 -3
  149. data/lib/doing/string/highlight.rb +95 -0
  150. data/lib/doing/string/query.rb +129 -0
  151. data/lib/doing/string/string.rb +12 -0
  152. data/lib/doing/string/tags.rb +164 -0
  153. data/lib/doing/string/transform.rb +168 -0
  154. data/lib/doing/string/truncate.rb +75 -0
  155. data/lib/doing/string/url.rb +82 -0
  156. data/lib/doing/template_string.rb +2 -24
  157. data/lib/doing/types.rb +9 -0
  158. data/lib/doing/util.rb +20 -16
  159. data/lib/doing/version.rb +1 -1
  160. data/lib/doing/wwid.rb +91 -51
  161. data/lib/doing.rb +5 -6
  162. data/lib/examples/commands/wiki.rb +6 -7
  163. data/lib/examples/plugins/wiki_export/wiki_export.rb +1 -1
  164. data/lib/helpers/threaded_tests.rb +69 -79
  165. data/lib/helpers/threaded_tests_string.rb +50 -0
  166. data/scripts/deploy.rb +107 -0
  167. data/scripts/runtests.sh +4 -0
  168. metadata +65 -8
  169. data/lib/doing/string.rb +0 -765
  170. 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.nil? || value.empty?
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.nil? || tags_color.empty?
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 if cli.nil?
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
- .find { |cmd| exec_available(cmd.split.first) }
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.dup, other_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
- Doing.logger.warn('Deprecated:', "Please update your configuration, 'editors' should be a mapping. Delete the key and run `doing config --update`.")
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
- # Doing.logger.debug('Editor:', "Using #{editor} from config 'editors->#{editor_for}'")
174
- return editor unless editor.nil? || editor.empty?
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
- # Doing.logger.debug('Editor:', "Using #{editor} from config: 'editors->default'")
180
- return editor unless editor.nil? || editor.empty?
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
- unless editor.nil? || editor.empty?
186
- # Doing.logger.debug('Editor:', "Found editor in environment variables: #{editor}")
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('ENV:', 'No EDITOR environment variable, testing available editors')
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
- return TTY::Which.which(ed) if TTY::Which.which(ed)
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('ENV:', "#{ed} not available")
199
+ Doing.logger.debug('Editor:', "#{ed} not available")
196
200
  end
197
201
 
198
202
  nil
data/lib/doing/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Doing
2
- VERSION = '2.1.24'
2
+ VERSION = '2.1.28'
3
3
  end