doing 2.1.2pre → 2.1.6pre
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 +19 -15
- data/.yardoc/object_types +0 -0
- data/.yardoc/objects/root.dat +0 -0
- data/.yardopts +1 -1
- data/CHANGELOG.md +62 -14
- data/Gemfile.lock +25 -1
- data/README.md +5 -1
- data/Rakefile +2 -0
- data/bin/doing +429 -142
- data/docs/_config.yml +1 -0
- data/{doc → docs/doc}/Array.html +63 -1
- data/docs/doc/BooleanTermParser/Clause.html +293 -0
- data/docs/doc/BooleanTermParser/Operator.html +172 -0
- data/docs/doc/BooleanTermParser/Query.html +417 -0
- data/docs/doc/BooleanTermParser/QueryParser.html +135 -0
- data/docs/doc/BooleanTermParser/QueryTransformer.html +124 -0
- data/docs/doc/BooleanTermParser.html +115 -0
- data/docs/doc/Doing/CLIFormat.html +131 -0
- data/{doc → docs/doc}/Doing/Color.html +2 -2
- data/{doc → docs/doc}/Doing/Completion.html +1 -1
- data/{doc → docs/doc}/Doing/Configuration.html +163 -69
- data/{doc → docs/doc}/Doing/Content.html +0 -0
- data/{doc → docs/doc}/Doing/Errors/DoingNoTraceError.html +1 -1
- data/{doc → docs/doc}/Doing/Errors/DoingRuntimeError.html +1 -1
- data/{doc → docs/doc}/Doing/Errors/DoingStandardError.html +1 -1
- data/{doc → docs/doc}/Doing/Errors/EmptyInput.html +1 -1
- data/{doc → docs/doc}/Doing/Errors/NoResults.html +1 -1
- data/{doc → docs/doc}/Doing/Errors/PluginException.html +1 -1
- data/{doc → docs/doc}/Doing/Errors/UserCancelled.html +1 -1
- data/{doc → docs/doc}/Doing/Errors/WrongCommand.html +1 -1
- data/{doc → docs/doc}/Doing/Errors.html +1 -1
- data/{doc → docs/doc}/Doing/Hooks.html +1 -1
- data/{doc → docs/doc}/Doing/Item.html +135 -89
- data/{doc → docs/doc}/Doing/Items.html +36 -2
- data/{doc → docs/doc}/Doing/LogAdapter.html +70 -1
- data/{doc → docs/doc}/Doing/Note.html +5 -134
- data/{doc → docs/doc}/Doing/Pager.html +1 -1
- data/{doc → docs/doc}/Doing/Plugins.html +431 -35
- data/{doc → docs/doc}/Doing/Prompt.html +70 -18
- data/{doc → docs/doc}/Doing/Section.html +1 -1
- data/docs/doc/Doing/TemplateString.html +713 -0
- data/docs/doc/Doing/Util/Backup.html +686 -0
- data/{doc → docs/doc}/Doing/Util.html +16 -4
- data/{doc → docs/doc}/Doing/WWID.html +133 -73
- data/{doc → docs/doc}/Doing/WWIDFile.html +0 -0
- data/{doc → docs/doc}/Doing.html +4 -4
- data/{doc → docs/doc}/GLI/Commands/MarkdownDocumentListener.html +1 -1
- data/{doc → docs/doc}/GLI/Commands.html +1 -1
- data/{doc → docs/doc}/GLI.html +1 -1
- data/{doc → docs/doc}/Hash.html +1 -1
- data/docs/doc/PhraseParser/Operator.html +172 -0
- data/docs/doc/PhraseParser/PhraseClause.html +303 -0
- data/docs/doc/PhraseParser/Query.html +495 -0
- data/docs/doc/PhraseParser/QueryParser.html +136 -0
- data/docs/doc/PhraseParser/QueryTransformer.html +124 -0
- data/docs/doc/PhraseParser/TermClause.html +293 -0
- data/docs/doc/PhraseParser.html +115 -0
- data/{doc → docs/doc}/Status.html +1 -1
- data/{doc → docs/doc}/String.html +319 -13
- data/{doc → docs/doc}/Symbol.html +35 -1
- data/{doc → docs/doc}/Time.html +70 -2
- data/{doc → docs/doc}/_index.html +132 -4
- data/docs/doc/class_list.html +51 -0
- data/{doc → docs/doc}/css/common.css +0 -0
- data/{doc → docs/doc}/css/full_list.css +0 -0
- data/{doc → docs/doc}/css/style.css +0 -0
- data/{doc → docs/doc}/file.README.html +6 -2
- data/{doc → docs/doc}/file_list.html +0 -0
- data/{doc → docs/doc}/frames.html +0 -0
- data/{doc → docs/doc}/index.html +6 -2
- data/{doc → docs/doc}/js/app.js +0 -0
- data/{doc → docs/doc}/js/full_list.js +0 -0
- data/{doc → docs/doc}/js/jquery.js +0 -0
- data/{doc → docs/doc}/method_list.html +684 -196
- data/{doc → docs/doc}/top-level-namespace.html +2 -2
- data/docs/index.md +60 -0
- data/doing.gemspec +3 -0
- data/doing.rdoc +222 -74
- data/example_plugin.rb +3 -1
- data/lib/completion/_doing.zsh +53 -41
- data/lib/completion/doing.bash +17 -6
- data/lib/completion/doing.fish +321 -2
- data/lib/doing/array.rb +9 -0
- data/lib/doing/boolean_term_parser.rb +86 -0
- data/lib/doing/completion/fish_completion.rb +46 -3
- data/lib/doing/completion/zsh_completion.rb +1 -1
- data/lib/doing/configuration.rb +48 -21
- data/lib/doing/item.rb +105 -10
- data/lib/doing/items.rb +6 -0
- data/lib/doing/log_adapter.rb +28 -0
- data/lib/doing/note.rb +31 -30
- data/lib/doing/phrase_parser.rb +124 -0
- data/lib/doing/plugin_manager.rb +84 -21
- data/lib/doing/plugins/export/dayone_export.rb +209 -0
- data/lib/doing/plugins/export/html_export.rb +2 -2
- data/lib/doing/plugins/export/json_export.rb +1 -0
- data/lib/doing/plugins/export/markdown_export.rb +1 -1
- data/lib/doing/plugins/export/template_export.rb +94 -86
- data/lib/doing/prompt.rb +26 -15
- data/lib/doing/string.rb +114 -29
- data/lib/doing/string_chronify.rb +5 -1
- data/lib/doing/symbol.rb +4 -0
- data/lib/doing/template_string.rb +197 -0
- data/lib/doing/time.rb +32 -0
- data/lib/doing/util.rb +6 -7
- data/lib/doing/util_backup.rb +287 -0
- data/lib/doing/version.rb +1 -1
- data/lib/doing/wwid.rb +105 -41
- data/lib/doing.rb +9 -0
- data/lib/examples/plugins/say_export.rb +1 -1
- data/lib/examples/plugins/wiki_export/wiki_export.rb +3 -3
- data/lib/templates/doing-dayone-entry.erb +6 -0
- data/lib/templates/doing-dayone.erb +5 -0
- metadata +136 -51
- data/doc/class_list.html +0 -51
data/lib/doing/configuration.rb
CHANGED
|
@@ -7,7 +7,11 @@ module Doing
|
|
|
7
7
|
class Configuration
|
|
8
8
|
attr_reader :settings
|
|
9
9
|
|
|
10
|
-
attr_writer :ignore_local
|
|
10
|
+
attr_writer :ignore_local, :config_file, :force_answer
|
|
11
|
+
|
|
12
|
+
def force_answer
|
|
13
|
+
@force_answer ||= false
|
|
14
|
+
end
|
|
11
15
|
|
|
12
16
|
MissingConfigFile = Class.new(RuntimeError)
|
|
13
17
|
|
|
@@ -25,11 +29,13 @@ module Doing
|
|
|
25
29
|
'plugin_path' => File.join(Util.user_home, '.config', 'doing', 'plugins'),
|
|
26
30
|
'command_path' => File.join(Util.user_home, '.config', 'doing', 'commands')
|
|
27
31
|
},
|
|
28
|
-
'doing_file' => '
|
|
32
|
+
'doing_file' => '~/.local/share/doing/what_was_i_doing.md',
|
|
33
|
+
'backup_dir' => '~/.local/share/doing/doing_backup',
|
|
29
34
|
'current_section' => 'Currently',
|
|
30
35
|
'paginate' => false,
|
|
31
36
|
'never_time' => [],
|
|
32
37
|
'never_finish' => [],
|
|
38
|
+
'date_tags' => ['done', 'defer(?:red)?', 'waiting'],
|
|
33
39
|
|
|
34
40
|
'timer_format' => 'text',
|
|
35
41
|
'interval_format' => 'text',
|
|
@@ -37,7 +43,8 @@ module Doing
|
|
|
37
43
|
'templates' => {
|
|
38
44
|
'default' => {
|
|
39
45
|
'date_format' => '%Y-%m-%d %H:%M',
|
|
40
|
-
'template' => '%
|
|
46
|
+
'template' => '%reset%cyan%shortdate %boldwhite%80║ title %dark%boldmagenta[%boldwhite%-10section%boldmagenta]%reset
|
|
47
|
+
%yellow%interval%boldred%duration%dark%white%80_14┃ note',
|
|
41
48
|
'wrap_width' => 0,
|
|
42
49
|
'order' => 'asc'
|
|
43
50
|
},
|
|
@@ -54,7 +61,8 @@ module Doing
|
|
|
54
61
|
},
|
|
55
62
|
'recent' => {
|
|
56
63
|
'date_format' => '%_I:%M%P',
|
|
57
|
-
'template' => '%shortdate
|
|
64
|
+
'template' => '%reset%cyan%shortdate %boldwhite%80║ title %dark%boldmagenta[%boldwhite%-10section%boldmagenta]%reset
|
|
65
|
+
%yellow%interval%boldred%duration%dark%white%80_14┃ note',
|
|
58
66
|
'wrap_width' => 88,
|
|
59
67
|
'count' => 10,
|
|
60
68
|
'order' => 'asc'
|
|
@@ -66,7 +74,7 @@ module Doing
|
|
|
66
74
|
'views' => {
|
|
67
75
|
'done' => {
|
|
68
76
|
'date_format' => '%_I:%M%P',
|
|
69
|
-
'template' => '%date | %title%note',
|
|
77
|
+
'template' => '%date | %title (%section)% 18: note',
|
|
70
78
|
'wrap_width' => 0,
|
|
71
79
|
'section' => 'All',
|
|
72
80
|
'count' => 0,
|
|
@@ -87,6 +95,11 @@ module Doing
|
|
|
87
95
|
'marker_color' => 'red',
|
|
88
96
|
'default_tags' => [],
|
|
89
97
|
'tag_sort' => 'name',
|
|
98
|
+
'search' => {
|
|
99
|
+
'matching' => 'pattern', # fuzzy, pattern, exact
|
|
100
|
+
'distance' => 3,
|
|
101
|
+
'case' => 'smart' # sensitive, ignore, smart
|
|
102
|
+
},
|
|
90
103
|
'include_notes' => true
|
|
91
104
|
}
|
|
92
105
|
|
|
@@ -100,24 +113,32 @@ module Doing
|
|
|
100
113
|
@config_file ||= default_config_file
|
|
101
114
|
end
|
|
102
115
|
|
|
103
|
-
def config_file=(file)
|
|
104
|
-
@config_file = file
|
|
105
|
-
end
|
|
106
|
-
|
|
107
116
|
def config_dir
|
|
108
117
|
@config_dir ||= File.join(Util.user_home, '.config', 'doing')
|
|
109
|
-
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
##
|
|
121
|
+
## Check if configuration enforces exact string matching
|
|
122
|
+
##
|
|
123
|
+
## @return [Boolean] exact matching enabled
|
|
124
|
+
##
|
|
125
|
+
def exact_match?
|
|
126
|
+
search_settings = @settings['search']
|
|
127
|
+
matching = search_settings.fetch('matching', 'pattern').normalize_matching
|
|
128
|
+
matching == :exact
|
|
110
129
|
end
|
|
111
130
|
|
|
112
131
|
def default_config_file
|
|
113
|
-
|
|
132
|
+
if File.exist?(config_dir) && !File.directory?(config_dir)
|
|
133
|
+
raise DoingRuntimeError, "#{config_dir} exists but is not a directory"
|
|
134
|
+
|
|
135
|
+
end
|
|
114
136
|
|
|
115
137
|
unless File.exist?(config_dir)
|
|
116
138
|
FileUtils.mkdir_p(config_dir)
|
|
117
139
|
Doing.logger.log_now(:warn, "Config directory created at #{config_dir}")
|
|
118
140
|
end
|
|
119
141
|
|
|
120
|
-
# File.join(config_dir, 'config.yml')
|
|
121
142
|
File.join(config_dir, 'config.yml')
|
|
122
143
|
end
|
|
123
144
|
|
|
@@ -131,10 +152,13 @@ module Doing
|
|
|
131
152
|
## @return [String] file path
|
|
132
153
|
##
|
|
133
154
|
def choose_config
|
|
155
|
+
return @config_file if @force_answer
|
|
156
|
+
|
|
134
157
|
if @additional_configs.count.positive?
|
|
135
|
-
choices = [@config_file]
|
|
136
|
-
choices.
|
|
137
|
-
|
|
158
|
+
choices = [@config_file].concat(@additional_configs)
|
|
159
|
+
res = Doing::Prompt.choose_from(choices.uniq.sort.reverse,
|
|
160
|
+
sorted: false,
|
|
161
|
+
prompt: 'Local configs found, select which to update > ')
|
|
138
162
|
|
|
139
163
|
raise UserCancelled, 'Cancelled' unless res
|
|
140
164
|
|
|
@@ -214,11 +238,15 @@ module Doing
|
|
|
214
238
|
cfg.nil? ? nil : { real_path[-1] => cfg }
|
|
215
239
|
end
|
|
216
240
|
|
|
217
|
-
# It takes the input, fills in the defaults where values
|
|
241
|
+
# It takes the input, fills in the defaults where values
|
|
242
|
+
# do not exist.
|
|
243
|
+
#
|
|
244
|
+
# @param user_config a Hash or Configuration of
|
|
245
|
+
# overrides.
|
|
218
246
|
#
|
|
219
|
-
#
|
|
247
|
+
# @return [Hash] a Configuration filled with
|
|
248
|
+
# defaults.
|
|
220
249
|
#
|
|
221
|
-
# Returns a Configuration filled with defaults.
|
|
222
250
|
def from(user_config)
|
|
223
251
|
Util.deep_merge_hashes(DEFAULTS, Configuration[user_config].stringify_keys)
|
|
224
252
|
end
|
|
@@ -233,14 +261,13 @@ module Doing
|
|
|
233
261
|
old_file = File.join(Util.user_home, '.doingrc')
|
|
234
262
|
return unless File.exist?(old_file)
|
|
235
263
|
|
|
236
|
-
wwid = Doing::WWID.new
|
|
237
264
|
Doing.logger.log_now(:warn, 'Deprecated:', "main config file location has changed to #{config_file}")
|
|
238
|
-
res =
|
|
265
|
+
res = Prompt.yn("Move #{old_file} to new location, preserving settings?", default_response: true)
|
|
239
266
|
|
|
240
267
|
return unless res
|
|
241
268
|
|
|
242
269
|
if File.exist?(default_config_file)
|
|
243
|
-
res =
|
|
270
|
+
res = Prompt.yn("#{default_config_file} already exists, overwrite it?", default_response: false)
|
|
244
271
|
|
|
245
272
|
unless res
|
|
246
273
|
@config_file = old_file
|
data/lib/doing/item.rb
CHANGED
|
@@ -7,7 +7,7 @@ module Doing
|
|
|
7
7
|
class Item
|
|
8
8
|
attr_accessor :date, :title, :section, :note
|
|
9
9
|
|
|
10
|
-
attr_reader :id
|
|
10
|
+
# attr_reader :id
|
|
11
11
|
|
|
12
12
|
##
|
|
13
13
|
## Initialize an item with date, title, section, and
|
|
@@ -116,7 +116,7 @@ module Doing
|
|
|
116
116
|
## Add (or remove) tags from the title of the item
|
|
117
117
|
##
|
|
118
118
|
## @param tags [Array] The tags to apply
|
|
119
|
-
## @param
|
|
119
|
+
## @param options Additional options
|
|
120
120
|
##
|
|
121
121
|
## @option options :date [Boolean] Include timestamp?
|
|
122
122
|
## @option options :single [Boolean] Log as a single change?
|
|
@@ -162,6 +162,10 @@ module Doing
|
|
|
162
162
|
@title.scan(/(?<= |\A)@([^\s(]+)/).map { |tag| tag[0] }.sort.uniq
|
|
163
163
|
end
|
|
164
164
|
|
|
165
|
+
def tag_array
|
|
166
|
+
tags.tags_to_array
|
|
167
|
+
end
|
|
168
|
+
|
|
165
169
|
##
|
|
166
170
|
## Test if item contains tag(s)
|
|
167
171
|
##
|
|
@@ -172,6 +176,13 @@ module Doing
|
|
|
172
176
|
## @return [Boolean] true if tag/bool combination passes
|
|
173
177
|
##
|
|
174
178
|
def tags?(tags, bool = :and, negate: false)
|
|
179
|
+
if bool == :pattern
|
|
180
|
+
tags = tags.join(' ') if tags.is_a?(Array)
|
|
181
|
+
matches = tag_pattern?(tags.gsub(/ *, */, ' '))
|
|
182
|
+
|
|
183
|
+
return negate ? !matches : matches
|
|
184
|
+
end
|
|
185
|
+
|
|
175
186
|
tags = split_tags(tags)
|
|
176
187
|
bool = bool.normalize_bool
|
|
177
188
|
|
|
@@ -186,6 +197,10 @@ module Doing
|
|
|
186
197
|
negate ? !matches : matches
|
|
187
198
|
end
|
|
188
199
|
|
|
200
|
+
def ignore_case(search, case_type)
|
|
201
|
+
(case_type == :smart && search !~ /[A-Z]/) || case_type == :ignore
|
|
202
|
+
end
|
|
203
|
+
|
|
189
204
|
##
|
|
190
205
|
## Test if item matches search string
|
|
191
206
|
##
|
|
@@ -197,9 +212,30 @@ module Doing
|
|
|
197
212
|
##
|
|
198
213
|
## @return [Boolean] matches search criteria
|
|
199
214
|
##
|
|
200
|
-
def search(search, distance:
|
|
201
|
-
|
|
202
|
-
|
|
215
|
+
def search(search, distance: nil, negate: false, case_type: nil)
|
|
216
|
+
prefs = Doing.config.settings['search'] || {}
|
|
217
|
+
matching = prefs.fetch('matching', 'pattern').normalize_matching
|
|
218
|
+
distance ||= prefs.fetch('distance', 3).to_i
|
|
219
|
+
case_type ||= prefs.fetch('case', 'smart').normalize_case
|
|
220
|
+
|
|
221
|
+
if search.is_rx? || matching == :fuzzy
|
|
222
|
+
matches = @title + @note.to_s =~ search.to_rx(distance: distance, case_type: case_type)
|
|
223
|
+
else
|
|
224
|
+
query = to_phrase_query(search.strip)
|
|
225
|
+
|
|
226
|
+
if query[:must].nil? && query[:must_not].nil?
|
|
227
|
+
query[:must] = query[:should]
|
|
228
|
+
query[:should] = []
|
|
229
|
+
end
|
|
230
|
+
matches = no_searches?(query[:must_not], case_type: case_type)
|
|
231
|
+
matches &&= all_searches?(query[:must], case_type: case_type)
|
|
232
|
+
matches &&= any_searches?(query[:should], case_type: case_type)
|
|
233
|
+
end
|
|
234
|
+
# if search =~ /(?<=\A| )[+-]\S/
|
|
235
|
+
# else
|
|
236
|
+
# text = @title + @note.to_s
|
|
237
|
+
# matches = text =~ search.to_rx(distance: distance, case_type: case_type)
|
|
238
|
+
# end
|
|
203
239
|
|
|
204
240
|
# if search.is_rx? || !fuzzy
|
|
205
241
|
# matches = text =~ search.to_rx(distance: distance, case_type: case_type)
|
|
@@ -286,33 +322,92 @@ module Doing
|
|
|
286
322
|
start = @date
|
|
287
323
|
|
|
288
324
|
t = (done - start).to_i
|
|
289
|
-
t
|
|
325
|
+
t.positive? ? t : nil
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
def all_searches?(searches, case_type: :smart)
|
|
329
|
+
return true if searches.nil? || searches.empty?
|
|
330
|
+
|
|
331
|
+
text = @title + @note.to_s
|
|
332
|
+
searches.each do |s|
|
|
333
|
+
rx = Regexp.new(s.wildcard_to_rx, ignore_case(s, case_type))
|
|
334
|
+
return false unless text =~ rx
|
|
335
|
+
end
|
|
336
|
+
true
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
def no_searches?(searches, case_type: :smart)
|
|
340
|
+
return true if searches.nil? || searches.empty?
|
|
341
|
+
|
|
342
|
+
text = @title + @note.to_s
|
|
343
|
+
searches.each do |s|
|
|
344
|
+
rx = Regexp.new(s.wildcard_to_rx, ignore_case(s, case_type))
|
|
345
|
+
return false if text =~ rx
|
|
346
|
+
end
|
|
347
|
+
true
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
def any_searches?(searches, case_type: :smart)
|
|
351
|
+
return true if searches.nil? || searches.empty?
|
|
352
|
+
|
|
353
|
+
text = @title + @note.to_s
|
|
354
|
+
searches.each do |s|
|
|
355
|
+
rx = Regexp.new(s.wildcard_to_rx, ignore_case(s, case_type))
|
|
356
|
+
return true if text =~ rx
|
|
357
|
+
end
|
|
358
|
+
false
|
|
290
359
|
end
|
|
291
360
|
|
|
292
361
|
def all_tags?(tags)
|
|
362
|
+
return true if tags.nil? || tags.empty?
|
|
363
|
+
|
|
293
364
|
tags.each do |tag|
|
|
294
|
-
return false unless @title =~ /@#{tag}/
|
|
365
|
+
return false unless @title =~ /@#{tag.wildcard_to_rx}(?= |\(|\Z)/i
|
|
295
366
|
end
|
|
296
367
|
true
|
|
297
368
|
end
|
|
298
369
|
|
|
299
370
|
def no_tags?(tags)
|
|
371
|
+
return true if tags.nil? || tags.empty?
|
|
372
|
+
|
|
300
373
|
tags.each do |tag|
|
|
301
|
-
return false if @title =~ /@#{tag}/
|
|
374
|
+
return false if @title =~ /@#{tag.wildcard_to_rx}(?= |\(|\Z)/i
|
|
302
375
|
end
|
|
303
376
|
true
|
|
304
377
|
end
|
|
305
378
|
|
|
306
379
|
def any_tags?(tags)
|
|
380
|
+
return true if tags.nil? || tags.empty?
|
|
381
|
+
|
|
307
382
|
tags.each do |tag|
|
|
308
|
-
return true if @title =~ /@#{tag}/
|
|
383
|
+
return true if @title =~ /@#{tag.wildcard_to_rx}(?= |\(|\Z)/i
|
|
309
384
|
end
|
|
310
385
|
false
|
|
311
386
|
end
|
|
312
387
|
|
|
388
|
+
def to_query(query)
|
|
389
|
+
parser = BooleanTermParser::QueryParser.new
|
|
390
|
+
transformer = BooleanTermParser::QueryTransformer.new
|
|
391
|
+
parse_tree = parser.parse(query)
|
|
392
|
+
transformer.apply(parse_tree).to_elasticsearch
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
def to_phrase_query(query)
|
|
396
|
+
parser = PhraseParser::QueryParser.new
|
|
397
|
+
transformer = PhraseParser::QueryTransformer.new
|
|
398
|
+
parse_tree = parser.parse(query)
|
|
399
|
+
transformer.apply(parse_tree).to_elasticsearch
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
def tag_pattern?(tags)
|
|
403
|
+
query = to_query(tags)
|
|
404
|
+
|
|
405
|
+
no_tags?(query[:must_not]) && all_tags?(query[:must]) && any_tags?(query[:should])
|
|
406
|
+
end
|
|
407
|
+
|
|
313
408
|
def split_tags(tags)
|
|
314
409
|
tags = tags.split(/ *, */) if tags.is_a? String
|
|
315
|
-
tags.map { |t| t.strip.
|
|
410
|
+
tags.map { |t| t.strip.add_at }
|
|
316
411
|
end
|
|
317
412
|
end
|
|
318
413
|
end
|
data/lib/doing/items.rb
CHANGED
data/lib/doing/log_adapter.rb
CHANGED
|
@@ -38,6 +38,7 @@ module Doing
|
|
|
38
38
|
rotated
|
|
39
39
|
skipped
|
|
40
40
|
updated
|
|
41
|
+
exported
|
|
41
42
|
].freeze
|
|
42
43
|
|
|
43
44
|
#
|
|
@@ -265,6 +266,31 @@ module Doing
|
|
|
265
266
|
end
|
|
266
267
|
end
|
|
267
268
|
|
|
269
|
+
def benchmark(key, state)
|
|
270
|
+
return unless ENV['DOING_BENCHMARK']
|
|
271
|
+
|
|
272
|
+
@benchmarks ||= {}
|
|
273
|
+
@benchmarks[key] ||= { start: nil, finish: nil }
|
|
274
|
+
@benchmarks[key][state] = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def log_benchmarks
|
|
278
|
+
if ENV['DOING_BENCHMARK']
|
|
279
|
+
output = []
|
|
280
|
+
@benchmarks.each do |k, timers|
|
|
281
|
+
if timers[:finish] && timers[:start]
|
|
282
|
+
output << "#{k}: #{timers[:finish] - timers[:start]}"
|
|
283
|
+
else
|
|
284
|
+
output << "#{k}: error"
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
output.each do |msg|
|
|
288
|
+
$stdout.puts color_message(:debug, 'Benchmark:', msg)
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
|
|
268
294
|
def log_change(tags_added: [], tags_removed: [], count: 1, item: nil, single: false)
|
|
269
295
|
if tags_added.empty? && tags_removed.empty?
|
|
270
296
|
count(:skipped, level: :debug, message: '%count %items with no change', count: count)
|
|
@@ -319,6 +345,8 @@ module Doing
|
|
|
319
345
|
['Archived:', data[:message] || 'completed and archived %count %items']
|
|
320
346
|
when :skipped
|
|
321
347
|
['Skipped:', data[:message] || '%count %items were unchanged']
|
|
348
|
+
when :exported
|
|
349
|
+
['Exported:', data[:message] || '%count %items were exported']
|
|
322
350
|
end
|
|
323
351
|
end
|
|
324
352
|
|
data/lib/doing/note.rb
CHANGED
|
@@ -22,7 +22,7 @@ module Doing
|
|
|
22
22
|
## Add note contents, optionally replacing existing note
|
|
23
23
|
##
|
|
24
24
|
## @param note [Array] The note to add, can be
|
|
25
|
-
##
|
|
25
|
+
## String, Array, or Note
|
|
26
26
|
## @param replace [Boolean] replace existing
|
|
27
27
|
## content
|
|
28
28
|
##
|
|
@@ -36,32 +36,7 @@ module Doing
|
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
##
|
|
39
|
-
##
|
|
40
|
-
##
|
|
41
|
-
## @param lines [Array] Array of strings
|
|
42
|
-
##
|
|
43
|
-
def append(lines)
|
|
44
|
-
concat(lines)
|
|
45
|
-
replace compress
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
##
|
|
49
|
-
## Append a string to the note content
|
|
50
|
-
##
|
|
51
|
-
## @param input [String] The input string,
|
|
52
|
-
## newlines will be split
|
|
53
|
-
##
|
|
54
|
-
def append_string(input)
|
|
55
|
-
concat(input.split(/\n/).map(&:strip))
|
|
56
|
-
replace compress
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
def compress!
|
|
60
|
-
replace compress
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
##
|
|
64
|
-
## Remove blank lines and comment lines (#)
|
|
39
|
+
## Remove blank lines and comments (#)
|
|
65
40
|
##
|
|
66
41
|
## @return [Array] compressed array
|
|
67
42
|
##
|
|
@@ -69,8 +44,8 @@ module Doing
|
|
|
69
44
|
delete_if { |l| l =~ /^\s*$/ || l =~ /^#/ }
|
|
70
45
|
end
|
|
71
46
|
|
|
72
|
-
def
|
|
73
|
-
replace
|
|
47
|
+
def compress!
|
|
48
|
+
replace compress
|
|
74
49
|
end
|
|
75
50
|
|
|
76
51
|
##
|
|
@@ -83,6 +58,10 @@ module Doing
|
|
|
83
58
|
map(&:strip)
|
|
84
59
|
end
|
|
85
60
|
|
|
61
|
+
def strip_lines!
|
|
62
|
+
replace strip_lines
|
|
63
|
+
end
|
|
64
|
+
|
|
86
65
|
##
|
|
87
66
|
## Note as multi-line string
|
|
88
67
|
def to_s
|
|
@@ -101,11 +80,33 @@ module Doing
|
|
|
101
80
|
## @param other [Note] The other Note
|
|
102
81
|
##
|
|
103
82
|
## @return [Boolean] true if equal
|
|
104
|
-
##
|
|
105
83
|
def equal?(other)
|
|
106
84
|
return false unless other.is_a?(Note)
|
|
107
85
|
|
|
108
86
|
to_s == other.to_s
|
|
109
87
|
end
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
##
|
|
92
|
+
## Append an array of strings to note
|
|
93
|
+
##
|
|
94
|
+
## @param lines [Array] Array of strings
|
|
95
|
+
##
|
|
96
|
+
def append(lines)
|
|
97
|
+
concat(lines)
|
|
98
|
+
replace compress
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
##
|
|
102
|
+
## Append a string to the note content
|
|
103
|
+
##
|
|
104
|
+
## @param input [String] The input string,
|
|
105
|
+
## newlines will be split
|
|
106
|
+
##
|
|
107
|
+
def append_string(input)
|
|
108
|
+
concat(input.split(/\n/).map(&:strip))
|
|
109
|
+
replace compress
|
|
110
|
+
end
|
|
110
111
|
end
|
|
111
112
|
end
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'parslet'
|
|
4
|
+
|
|
5
|
+
module PhraseParser
|
|
6
|
+
# This parser adds quoted phrases (using matched double quotes) in addition to
|
|
7
|
+
# terms. This is done creating multiple types of clauses instead of just one.
|
|
8
|
+
# A phrase clause generates an Elasticsearch match_phrase query.
|
|
9
|
+
class QueryParser < Parslet::Parser
|
|
10
|
+
rule(:term) { match('[^\s"]').repeat(1).as(:term) }
|
|
11
|
+
rule(:quote) { str('"') }
|
|
12
|
+
rule(:operator) { (str('+') | str('-')).as(:operator) }
|
|
13
|
+
rule(:phrase) do
|
|
14
|
+
(quote >> (term >> space.maybe).repeat >> quote).as(:phrase)
|
|
15
|
+
end
|
|
16
|
+
rule(:clause) { (operator.maybe >> (phrase | term)).as(:clause) }
|
|
17
|
+
rule(:space) { match('\s').repeat(1) }
|
|
18
|
+
rule(:query) { (clause >> space.maybe).repeat.as(:query) }
|
|
19
|
+
root(:query)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
class QueryTransformer < Parslet::Transform
|
|
23
|
+
rule(:clause => subtree(:clause)) do
|
|
24
|
+
if clause[:term]
|
|
25
|
+
TermClause.new(clause[:operator]&.to_s, clause[:term].to_s)
|
|
26
|
+
elsif clause[:phrase]
|
|
27
|
+
phrase = clause[:phrase].map { |p| p[:term].to_s }.join(' ')
|
|
28
|
+
PhraseClause.new(clause[:operator]&.to_s, phrase)
|
|
29
|
+
else
|
|
30
|
+
raise "Unexpected clause type: '#{clause}'"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
rule(query: sequence(:clauses)) { Query.new(clauses) }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
class Operator
|
|
37
|
+
def self.symbol(str)
|
|
38
|
+
case str
|
|
39
|
+
when '+'
|
|
40
|
+
:must
|
|
41
|
+
when '-'
|
|
42
|
+
:must_not
|
|
43
|
+
when nil
|
|
44
|
+
:should
|
|
45
|
+
else
|
|
46
|
+
raise "Unknown operator: #{str}"
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
class TermClause
|
|
52
|
+
attr_accessor :operator, :term
|
|
53
|
+
|
|
54
|
+
def initialize(operator, term)
|
|
55
|
+
self.operator = Operator.symbol(operator)
|
|
56
|
+
self.term = term
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Phrase
|
|
61
|
+
class PhraseClause
|
|
62
|
+
attr_accessor :operator, :phrase
|
|
63
|
+
|
|
64
|
+
def initialize(operator, phrase)
|
|
65
|
+
self.operator = Operator.symbol(operator)
|
|
66
|
+
self.phrase = phrase
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
## Query object
|
|
71
|
+
class Query
|
|
72
|
+
attr_accessor :should_clauses, :must_not_clauses, :must_clauses
|
|
73
|
+
|
|
74
|
+
def initialize(clauses)
|
|
75
|
+
grouped = clauses.chunk(&:operator).to_h
|
|
76
|
+
self.should_clauses = grouped.fetch(:should, [])
|
|
77
|
+
self.must_not_clauses = grouped.fetch(:must_not, [])
|
|
78
|
+
self.must_clauses = grouped.fetch(:must, [])
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def to_elasticsearch
|
|
82
|
+
query = {}
|
|
83
|
+
|
|
84
|
+
if should_clauses.any?
|
|
85
|
+
query[:should] = should_clauses.map do |clause|
|
|
86
|
+
clause_to_query(clause)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
if must_clauses.any?
|
|
91
|
+
query[:must] = must_clauses.map do |clause|
|
|
92
|
+
clause_to_query(clause)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
if must_not_clauses.any?
|
|
97
|
+
query[:must_not] = must_not_clauses.map do |clause|
|
|
98
|
+
clause_to_query(clause)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
query
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def clause_to_query(clause)
|
|
106
|
+
case clause
|
|
107
|
+
when TermClause
|
|
108
|
+
match(clause.term)
|
|
109
|
+
when PhraseClause
|
|
110
|
+
match_phrase(clause.phrase)
|
|
111
|
+
else
|
|
112
|
+
raise "Unknown clause type: #{clause}"
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def match(term)
|
|
117
|
+
term
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def match_phrase(phrase)
|
|
121
|
+
phrase
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|