doing 2.1.3 → 2.1.6
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 +48 -0
- data/Gemfile.lock +25 -1
- data/README.md +5 -1
- 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 +157 -11
- 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 +134 -73
- 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 +35 -1
- 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 +285 -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 +624 -136
- 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 +45 -14
- data/lib/doing/item.rb +104 -9
- 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 +90 -85
- data/lib/doing/prompt.rb +17 -6
- data/lib/doing/string.rb +84 -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, :config_file
|
|
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,14 @@ 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',
|
|
35
|
+
'history_size' => 15,
|
|
30
36
|
'paginate' => false,
|
|
31
37
|
'never_time' => [],
|
|
32
38
|
'never_finish' => [],
|
|
39
|
+
'date_tags' => ['done', 'defer(?:red)?', 'waiting'],
|
|
33
40
|
|
|
34
41
|
'timer_format' => 'text',
|
|
35
42
|
'interval_format' => 'text',
|
|
@@ -37,7 +44,8 @@ module Doing
|
|
|
37
44
|
'templates' => {
|
|
38
45
|
'default' => {
|
|
39
46
|
'date_format' => '%Y-%m-%d %H:%M',
|
|
40
|
-
'template' => '%
|
|
47
|
+
'template' => '%reset%cyan%shortdate %boldwhite%80║ title %dark%boldmagenta[%boldwhite%-10section%boldmagenta]%reset
|
|
48
|
+
%yellow%interval%boldred%duration%dark%white%80_14┃ note',
|
|
41
49
|
'wrap_width' => 0,
|
|
42
50
|
'order' => 'asc'
|
|
43
51
|
},
|
|
@@ -54,7 +62,8 @@ module Doing
|
|
|
54
62
|
},
|
|
55
63
|
'recent' => {
|
|
56
64
|
'date_format' => '%_I:%M%P',
|
|
57
|
-
'template' => '%shortdate
|
|
65
|
+
'template' => '%reset%cyan%shortdate %boldwhite%80║ title %dark%boldmagenta[%boldwhite%-10section%boldmagenta]%reset
|
|
66
|
+
%yellow%interval%boldred%duration%dark%white%80_14┃ note',
|
|
58
67
|
'wrap_width' => 88,
|
|
59
68
|
'count' => 10,
|
|
60
69
|
'order' => 'asc'
|
|
@@ -66,7 +75,7 @@ module Doing
|
|
|
66
75
|
'views' => {
|
|
67
76
|
'done' => {
|
|
68
77
|
'date_format' => '%_I:%M%P',
|
|
69
|
-
'template' => '%date | %title%note',
|
|
78
|
+
'template' => '%date | %title (%section)% 18: note',
|
|
70
79
|
'wrap_width' => 0,
|
|
71
80
|
'section' => 'All',
|
|
72
81
|
'count' => 0,
|
|
@@ -87,6 +96,11 @@ module Doing
|
|
|
87
96
|
'marker_color' => 'red',
|
|
88
97
|
'default_tags' => [],
|
|
89
98
|
'tag_sort' => 'name',
|
|
99
|
+
'search' => {
|
|
100
|
+
'matching' => 'pattern', # fuzzy, pattern, exact
|
|
101
|
+
'distance' => 3,
|
|
102
|
+
'case' => 'smart' # sensitive, ignore, smart
|
|
103
|
+
},
|
|
90
104
|
'include_notes' => true
|
|
91
105
|
}
|
|
92
106
|
|
|
@@ -104,6 +118,17 @@ module Doing
|
|
|
104
118
|
@config_dir ||= File.join(Util.user_home, '.config', 'doing')
|
|
105
119
|
end
|
|
106
120
|
|
|
121
|
+
##
|
|
122
|
+
## Check if configuration enforces exact string matching
|
|
123
|
+
##
|
|
124
|
+
## @return [Boolean] exact matching enabled
|
|
125
|
+
##
|
|
126
|
+
def exact_match?
|
|
127
|
+
search_settings = @settings['search']
|
|
128
|
+
matching = search_settings.fetch('matching', 'pattern').normalize_matching
|
|
129
|
+
matching == :exact
|
|
130
|
+
end
|
|
131
|
+
|
|
107
132
|
def default_config_file
|
|
108
133
|
if File.exist?(config_dir) && !File.directory?(config_dir)
|
|
109
134
|
raise DoingRuntimeError, "#{config_dir} exists but is not a directory"
|
|
@@ -128,10 +153,13 @@ module Doing
|
|
|
128
153
|
## @return [String] file path
|
|
129
154
|
##
|
|
130
155
|
def choose_config
|
|
156
|
+
return @config_file if @force_answer
|
|
157
|
+
|
|
131
158
|
if @additional_configs.count.positive?
|
|
132
|
-
choices = [@config_file]
|
|
133
|
-
choices.
|
|
134
|
-
|
|
159
|
+
choices = [@config_file].concat(@additional_configs)
|
|
160
|
+
res = Doing::Prompt.choose_from(choices.uniq.sort.reverse,
|
|
161
|
+
sorted: false,
|
|
162
|
+
prompt: 'Local configs found, select which to update > ')
|
|
135
163
|
|
|
136
164
|
raise UserCancelled, 'Cancelled' unless res
|
|
137
165
|
|
|
@@ -211,11 +239,15 @@ module Doing
|
|
|
211
239
|
cfg.nil? ? nil : { real_path[-1] => cfg }
|
|
212
240
|
end
|
|
213
241
|
|
|
214
|
-
# It takes the input, fills in the defaults where values
|
|
242
|
+
# It takes the input, fills in the defaults where values
|
|
243
|
+
# do not exist.
|
|
244
|
+
#
|
|
245
|
+
# @param user_config a Hash or Configuration of
|
|
246
|
+
# overrides.
|
|
215
247
|
#
|
|
216
|
-
#
|
|
248
|
+
# @return [Hash] a Configuration filled with
|
|
249
|
+
# defaults.
|
|
217
250
|
#
|
|
218
|
-
# Returns a Configuration filled with defaults.
|
|
219
251
|
def from(user_config)
|
|
220
252
|
Util.deep_merge_hashes(DEFAULTS, Configuration[user_config].stringify_keys)
|
|
221
253
|
end
|
|
@@ -230,14 +262,13 @@ module Doing
|
|
|
230
262
|
old_file = File.join(Util.user_home, '.doingrc')
|
|
231
263
|
return unless File.exist?(old_file)
|
|
232
264
|
|
|
233
|
-
wwid = Doing::WWID.new
|
|
234
265
|
Doing.logger.log_now(:warn, 'Deprecated:', "main config file location has changed to #{config_file}")
|
|
235
|
-
res =
|
|
266
|
+
res = Prompt.yn("Move #{old_file} to new location, preserving settings?", default_response: true)
|
|
236
267
|
|
|
237
268
|
return unless res
|
|
238
269
|
|
|
239
270
|
if File.exist?(default_config_file)
|
|
240
|
-
res =
|
|
271
|
+
res = Prompt.yn("#{default_config_file} already exists, overwrite it?", default_response: false)
|
|
241
272
|
|
|
242
273
|
unless res
|
|
243
274
|
@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
|
|
@@ -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
|