doing 2.1.2pre → 2.1.6pre
Sign up to get free protection for your applications and to get access to all the features.
- 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
|