doing 2.1.2pre → 2.1.6pre

Sign up to get free protection for your applications and to get access to all the features.
Files changed (116) hide show
  1. checksums.yaml +4 -4
  2. data/.yardoc/checksums +19 -15
  3. data/.yardoc/object_types +0 -0
  4. data/.yardoc/objects/root.dat +0 -0
  5. data/.yardopts +1 -1
  6. data/CHANGELOG.md +62 -14
  7. data/Gemfile.lock +25 -1
  8. data/README.md +5 -1
  9. data/Rakefile +2 -0
  10. data/bin/doing +429 -142
  11. data/docs/_config.yml +1 -0
  12. data/{doc → docs/doc}/Array.html +63 -1
  13. data/docs/doc/BooleanTermParser/Clause.html +293 -0
  14. data/docs/doc/BooleanTermParser/Operator.html +172 -0
  15. data/docs/doc/BooleanTermParser/Query.html +417 -0
  16. data/docs/doc/BooleanTermParser/QueryParser.html +135 -0
  17. data/docs/doc/BooleanTermParser/QueryTransformer.html +124 -0
  18. data/docs/doc/BooleanTermParser.html +115 -0
  19. data/docs/doc/Doing/CLIFormat.html +131 -0
  20. data/{doc → docs/doc}/Doing/Color.html +2 -2
  21. data/{doc → docs/doc}/Doing/Completion.html +1 -1
  22. data/{doc → docs/doc}/Doing/Configuration.html +163 -69
  23. data/{doc → docs/doc}/Doing/Content.html +0 -0
  24. data/{doc → docs/doc}/Doing/Errors/DoingNoTraceError.html +1 -1
  25. data/{doc → docs/doc}/Doing/Errors/DoingRuntimeError.html +1 -1
  26. data/{doc → docs/doc}/Doing/Errors/DoingStandardError.html +1 -1
  27. data/{doc → docs/doc}/Doing/Errors/EmptyInput.html +1 -1
  28. data/{doc → docs/doc}/Doing/Errors/NoResults.html +1 -1
  29. data/{doc → docs/doc}/Doing/Errors/PluginException.html +1 -1
  30. data/{doc → docs/doc}/Doing/Errors/UserCancelled.html +1 -1
  31. data/{doc → docs/doc}/Doing/Errors/WrongCommand.html +1 -1
  32. data/{doc → docs/doc}/Doing/Errors.html +1 -1
  33. data/{doc → docs/doc}/Doing/Hooks.html +1 -1
  34. data/{doc → docs/doc}/Doing/Item.html +135 -89
  35. data/{doc → docs/doc}/Doing/Items.html +36 -2
  36. data/{doc → docs/doc}/Doing/LogAdapter.html +70 -1
  37. data/{doc → docs/doc}/Doing/Note.html +5 -134
  38. data/{doc → docs/doc}/Doing/Pager.html +1 -1
  39. data/{doc → docs/doc}/Doing/Plugins.html +431 -35
  40. data/{doc → docs/doc}/Doing/Prompt.html +70 -18
  41. data/{doc → docs/doc}/Doing/Section.html +1 -1
  42. data/docs/doc/Doing/TemplateString.html +713 -0
  43. data/docs/doc/Doing/Util/Backup.html +686 -0
  44. data/{doc → docs/doc}/Doing/Util.html +16 -4
  45. data/{doc → docs/doc}/Doing/WWID.html +133 -73
  46. data/{doc → docs/doc}/Doing/WWIDFile.html +0 -0
  47. data/{doc → docs/doc}/Doing.html +4 -4
  48. data/{doc → docs/doc}/GLI/Commands/MarkdownDocumentListener.html +1 -1
  49. data/{doc → docs/doc}/GLI/Commands.html +1 -1
  50. data/{doc → docs/doc}/GLI.html +1 -1
  51. data/{doc → docs/doc}/Hash.html +1 -1
  52. data/docs/doc/PhraseParser/Operator.html +172 -0
  53. data/docs/doc/PhraseParser/PhraseClause.html +303 -0
  54. data/docs/doc/PhraseParser/Query.html +495 -0
  55. data/docs/doc/PhraseParser/QueryParser.html +136 -0
  56. data/docs/doc/PhraseParser/QueryTransformer.html +124 -0
  57. data/docs/doc/PhraseParser/TermClause.html +293 -0
  58. data/docs/doc/PhraseParser.html +115 -0
  59. data/{doc → docs/doc}/Status.html +1 -1
  60. data/{doc → docs/doc}/String.html +319 -13
  61. data/{doc → docs/doc}/Symbol.html +35 -1
  62. data/{doc → docs/doc}/Time.html +70 -2
  63. data/{doc → docs/doc}/_index.html +132 -4
  64. data/docs/doc/class_list.html +51 -0
  65. data/{doc → docs/doc}/css/common.css +0 -0
  66. data/{doc → docs/doc}/css/full_list.css +0 -0
  67. data/{doc → docs/doc}/css/style.css +0 -0
  68. data/{doc → docs/doc}/file.README.html +6 -2
  69. data/{doc → docs/doc}/file_list.html +0 -0
  70. data/{doc → docs/doc}/frames.html +0 -0
  71. data/{doc → docs/doc}/index.html +6 -2
  72. data/{doc → docs/doc}/js/app.js +0 -0
  73. data/{doc → docs/doc}/js/full_list.js +0 -0
  74. data/{doc → docs/doc}/js/jquery.js +0 -0
  75. data/{doc → docs/doc}/method_list.html +684 -196
  76. data/{doc → docs/doc}/top-level-namespace.html +2 -2
  77. data/docs/index.md +60 -0
  78. data/doing.gemspec +3 -0
  79. data/doing.rdoc +222 -74
  80. data/example_plugin.rb +3 -1
  81. data/lib/completion/_doing.zsh +53 -41
  82. data/lib/completion/doing.bash +17 -6
  83. data/lib/completion/doing.fish +321 -2
  84. data/lib/doing/array.rb +9 -0
  85. data/lib/doing/boolean_term_parser.rb +86 -0
  86. data/lib/doing/completion/fish_completion.rb +46 -3
  87. data/lib/doing/completion/zsh_completion.rb +1 -1
  88. data/lib/doing/configuration.rb +48 -21
  89. data/lib/doing/item.rb +105 -10
  90. data/lib/doing/items.rb +6 -0
  91. data/lib/doing/log_adapter.rb +28 -0
  92. data/lib/doing/note.rb +31 -30
  93. data/lib/doing/phrase_parser.rb +124 -0
  94. data/lib/doing/plugin_manager.rb +84 -21
  95. data/lib/doing/plugins/export/dayone_export.rb +209 -0
  96. data/lib/doing/plugins/export/html_export.rb +2 -2
  97. data/lib/doing/plugins/export/json_export.rb +1 -0
  98. data/lib/doing/plugins/export/markdown_export.rb +1 -1
  99. data/lib/doing/plugins/export/template_export.rb +94 -86
  100. data/lib/doing/prompt.rb +26 -15
  101. data/lib/doing/string.rb +114 -29
  102. data/lib/doing/string_chronify.rb +5 -1
  103. data/lib/doing/symbol.rb +4 -0
  104. data/lib/doing/template_string.rb +197 -0
  105. data/lib/doing/time.rb +32 -0
  106. data/lib/doing/util.rb +6 -7
  107. data/lib/doing/util_backup.rb +287 -0
  108. data/lib/doing/version.rb +1 -1
  109. data/lib/doing/wwid.rb +105 -41
  110. data/lib/doing.rb +9 -0
  111. data/lib/examples/plugins/say_export.rb +1 -1
  112. data/lib/examples/plugins/wiki_export/wiki_export.rb +3 -3
  113. data/lib/templates/doing-dayone-entry.erb +6 -0
  114. data/lib/templates/doing-dayone.erb +5 -0
  115. metadata +136 -51
  116. data/doc/class_list.html +0 -51
@@ -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' => '~/what_was_i_doing.md',
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' => '%date | %title %interval%duration%note',
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: %title (%section) %interval%duration%note',
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
- # @config_dir ||= Util.user_home
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
- raise DoingRuntimeError, "#{config_dir} exists but is not a directory" if File.exist?(config_dir) && !File.directory?(config_dir)
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.concat(@additional_configs)
137
- res = Doing::Prompt.choose_from(choices.uniq.sort.reverse, sorted: false, prompt: 'Local configs found, select which to update > ')
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 do not exist.
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
- # user_config - a Hash or Configuration of overrides.
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 = wwid.yn("Move #{old_file} to new location, preserving settings?", default_response: true)
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 = wwid.yn("#{default_config_file} already exists, overwrite it?", default_response: false)
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 **options Additional options
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: 3, negate: false, case_type: :smart, fuzzy: false)
201
- text = @title + @note.to_s
202
- matches = text =~ search.to_rx(distance: distance, case_type: case_type)
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 > 0 ? t : nil
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.sub(/^@/, '') }
410
+ tags.map { |t| t.strip.add_at }
316
411
  end
317
412
  end
318
413
  end
data/lib/doing/items.rb CHANGED
@@ -105,6 +105,12 @@ module Doing
105
105
  new_item
106
106
  end
107
107
 
108
+ def all_tags
109
+ each_with_object([]) do |entry, tags|
110
+ tags.concat(entry.tags).sort!.uniq!
111
+ end
112
+ end
113
+
108
114
  # Output sections and items in Doing file format
109
115
  def to_s
110
116
  out = []
@@ -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
- ## string or array (Note)
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
- ## Append an array of strings to note
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 strip_lines!
73
- replace strip_lines
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