doing 2.1.1pre → 2.1.5pre

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.
Files changed (92) 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/CHANGELOG.md +58 -8
  6. data/Gemfile.lock +25 -1
  7. data/README.md +1 -1
  8. data/Rakefile +2 -0
  9. data/bin/doing +447 -149
  10. data/doc/Array.html +63 -1
  11. data/doc/BooleanTermParser/Clause.html +293 -0
  12. data/doc/BooleanTermParser/Operator.html +172 -0
  13. data/doc/BooleanTermParser/Query.html +417 -0
  14. data/doc/BooleanTermParser/QueryParser.html +135 -0
  15. data/doc/BooleanTermParser/QueryTransformer.html +124 -0
  16. data/doc/BooleanTermParser.html +115 -0
  17. data/doc/Doing/CLIFormat.html +131 -0
  18. data/doc/Doing/Color.html +2 -2
  19. data/doc/Doing/Completion.html +1 -1
  20. data/doc/Doing/Configuration.html +168 -73
  21. data/doc/Doing/Errors/DoingNoTraceError.html +1 -1
  22. data/doc/Doing/Errors/DoingRuntimeError.html +1 -1
  23. data/doc/Doing/Errors/DoingStandardError.html +1 -1
  24. data/doc/Doing/Errors/EmptyInput.html +1 -1
  25. data/doc/Doing/Errors/NoResults.html +1 -1
  26. data/doc/Doing/Errors/PluginException.html +1 -1
  27. data/doc/Doing/Errors/UserCancelled.html +1 -1
  28. data/doc/Doing/Errors/WrongCommand.html +1 -1
  29. data/doc/Doing/Errors.html +1 -1
  30. data/doc/Doing/Hooks.html +1 -1
  31. data/doc/Doing/Item.html +177 -86
  32. data/doc/Doing/Items.html +36 -2
  33. data/doc/Doing/LogAdapter.html +70 -1
  34. data/doc/Doing/Note.html +5 -134
  35. data/doc/Doing/Pager.html +1 -1
  36. data/doc/Doing/Plugins.html +380 -40
  37. data/doc/Doing/Prompt.html +70 -18
  38. data/doc/Doing/Section.html +1 -1
  39. data/doc/Doing/TemplateString.html +713 -0
  40. data/doc/Doing/Util/Backup.html +686 -0
  41. data/doc/Doing/Util.html +16 -4
  42. data/doc/Doing/WWID.html +133 -73
  43. data/doc/Doing.html +4 -4
  44. data/doc/GLI/Commands/MarkdownDocumentListener.html +1 -1
  45. data/doc/GLI/Commands.html +1 -1
  46. data/doc/GLI.html +1 -1
  47. data/doc/Hash.html +1 -1
  48. data/doc/PhraseParser/Operator.html +172 -0
  49. data/doc/PhraseParser/PhraseClause.html +303 -0
  50. data/doc/PhraseParser/Query.html +495 -0
  51. data/doc/PhraseParser/QueryParser.html +136 -0
  52. data/doc/PhraseParser/QueryTransformer.html +124 -0
  53. data/doc/PhraseParser/TermClause.html +293 -0
  54. data/doc/PhraseParser.html +115 -0
  55. data/doc/Status.html +1 -1
  56. data/doc/String.html +319 -13
  57. data/doc/Symbol.html +35 -1
  58. data/doc/Time.html +70 -2
  59. data/doc/_index.html +132 -4
  60. data/doc/class_list.html +1 -1
  61. data/doc/file.README.html +2 -2
  62. data/doc/index.html +2 -2
  63. data/doc/method_list.html +648 -160
  64. data/doc/top-level-namespace.html +2 -2
  65. data/doing.gemspec +3 -0
  66. data/doing.rdoc +263 -82
  67. data/lib/completion/doing.bash +18 -18
  68. data/lib/doing/array.rb +9 -0
  69. data/lib/doing/boolean_term_parser.rb +86 -0
  70. data/lib/doing/configuration.rb +63 -24
  71. data/lib/doing/item.rb +112 -10
  72. data/lib/doing/items.rb +6 -0
  73. data/lib/doing/log_adapter.rb +28 -0
  74. data/lib/doing/note.rb +31 -30
  75. data/lib/doing/phrase_parser.rb +124 -0
  76. data/lib/doing/plugin_manager.rb +57 -13
  77. data/lib/doing/plugins/export/dayone_export.rb +209 -0
  78. data/lib/doing/plugins/export/template_export.rb +113 -81
  79. data/lib/doing/prompt.rb +26 -13
  80. data/lib/doing/string.rb +114 -29
  81. data/lib/doing/string_chronify.rb +5 -1
  82. data/lib/doing/symbol.rb +4 -0
  83. data/lib/doing/template_string.rb +197 -0
  84. data/lib/doing/time.rb +32 -0
  85. data/lib/doing/util.rb +6 -7
  86. data/lib/doing/util_backup.rb +287 -0
  87. data/lib/doing/version.rb +1 -1
  88. data/lib/doing/wwid.rb +152 -55
  89. data/lib/doing.rb +9 -0
  90. data/lib/templates/doing-dayone-entry.erb +6 -0
  91. data/lib/templates/doing-dayone.erb +5 -0
  92. metadata +85 -2
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
@@ -28,26 +28,30 @@ module Doing
28
28
  plugins
29
29
  end
30
30
 
31
- # Public: Setup the plugin search path
31
+ # Setup the plugin search path
32
+ #
33
+ # @param add_dir [String] optional additional
34
+ # path to include
35
+ #
36
+ # @return [Array] Returns an Array of plugin search paths
32
37
  #
33
- # Returns an Array of plugin search paths
34
38
  def plugins_path(add_dir = nil)
35
39
  paths = Array(File.join(File.dirname(__FILE__), 'plugins'))
36
40
  paths << File.join(add_dir) if add_dir
37
41
  paths.map { |d| File.expand_path(d) }
38
42
  end
39
43
 
40
- ##
44
+
41
45
  # Register a plugin
42
46
  #
43
- # param: +[String|Array]+ title The name of the plugin (can be an array of names)
44
- #
45
- # param: +type+ The plugin type (:import, :export)
46
- #
47
- # param: +klass+ The class responding to :render or :import
47
+ # @param title [String|Array] The name of the
48
+ # plugin (can be an array of names)
49
+ # @param type [Symbol] The plugin type
50
+ # (:import, :export)
51
+ # @param klass [Class] The class responding to
52
+ # :render or :import
48
53
  #
49
- #
50
- # returns: Success boolean
54
+ # @return [Boolean] Success boolean
51
55
  #
52
56
  def register(title, type, klass)
53
57
  type = validate_plugin(title, type, klass)
@@ -90,6 +94,15 @@ module Doing
90
94
  type
91
95
  end
92
96
 
97
+ ##
98
+ ## Converts a partial symbol to a valid plugin type,
99
+ ## e.g. :imp => :import
100
+ ##
101
+ ## @param type [Symbol] the symbol to test
102
+ ## @param default [Symbol] fallback value
103
+ ##
104
+ ## @return [Symbol] :import or :export
105
+ ##
93
106
  def valid_type(type, default: nil)
94
107
  type ||= default
95
108
 
@@ -154,10 +167,13 @@ module Doing
154
167
  end
155
168
 
156
169
  ##
157
- ## Return a regular expression of all
158
- ## plugin triggers for type
170
+ ## Return a regular expression of all plugin triggers
171
+ ## for type
172
+ ##
173
+ ## @param type [Symbol] The type :import or
174
+ ## :export
159
175
  ##
160
- ## @param type The type :import or :export
176
+ ## @return [Regexp] regular expression
161
177
  ##
162
178
  def plugin_regex(type: :export)
163
179
  type = valid_type(type)
@@ -168,6 +184,14 @@ module Doing
168
184
  Regexp.new("^(?:#{pattern.join('|')})$", true)
169
185
  end
170
186
 
187
+ ##
188
+ ## Return array of available template names
189
+ ##
190
+ ## @param type [Symbol] Plugin type (:import,
191
+ ## :export)
192
+ ##
193
+ ## @return [Array<String>] template names
194
+ ##
171
195
  def plugin_templates(type: :export)
172
196
  type = valid_type(type)
173
197
  templates = []
@@ -181,6 +205,15 @@ module Doing
181
205
  templates
182
206
  end
183
207
 
208
+ ##
209
+ ## Return a regular expression of all template
210
+ ## triggers for type
211
+ ##
212
+ ## @param type [Symbol] The type :import or
213
+ ## :export
214
+ ##
215
+ ## @return [Regexp] regular expression
216
+ ##
184
217
  def template_regex(type: :export)
185
218
  type = valid_type(type)
186
219
  pattern = []
@@ -193,6 +226,17 @@ module Doing
193
226
  Regexp.new("^(?:#{pattern.join('|')})$", true)
194
227
  end
195
228
 
229
+ ##
230
+ ## Find and return the appropriate template for a
231
+ ## trigger string. Outputs a string that can be
232
+ ## written out to the terminal for redirection
233
+ ##
234
+ ## @param trigger [String] The trigger to test
235
+ ## @param type [Symbol] the plugin type
236
+ ## (:import, :export)
237
+ ##
238
+ ## @return [String] string content of template for trigger
239
+ ##
196
240
  def template_for_trigger(trigger, type: :export)
197
241
  type = valid_type(type)
198
242
  plugs = plugins[type].clone
@@ -0,0 +1,209 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ # title: Day One Export
6
+ # description: Export entries to Day One plist for auto import
7
+ # author: Brett Terpstra
8
+ # url: https://brettterpstra.com
9
+ module Doing
10
+ class DayOneRenderer
11
+ attr_accessor :items, :page_title, :totals
12
+
13
+ def initialize(page_title, items, totals)
14
+ @page_title = page_title
15
+ @items = items
16
+ @totals = totals
17
+ end
18
+
19
+ def get_binding
20
+ binding()
21
+ end
22
+ end
23
+
24
+ class DayoneExport
25
+ include Doing::Util
26
+
27
+ def self.settings
28
+ {
29
+ trigger: 'day(?:one)?(?:-(?:days?|entries))?',
30
+ templates: [
31
+ { name: 'dayone', trigger: 'day(?:one)?$' },
32
+ { name: 'dayone_entry', trigger: 'day(?:one)-entr(?:y|ies)?$'}
33
+ ]
34
+ }
35
+ end
36
+
37
+ def self.template(trigger)
38
+ case trigger
39
+ when /day(?:one)-entr(?:y|ies)?$/
40
+ IO.read(File.join(File.dirname(__FILE__), '../../../templates/doing-dayone-entry.erb'))
41
+ else
42
+ IO.read(File.join(File.dirname(__FILE__), '../../../templates/doing-dayone.erb'))
43
+ end
44
+ end
45
+
46
+ def self.render(wwid, items, variables: {})
47
+
48
+ return if items.nil?
49
+
50
+ opt = variables[:options]
51
+ trigger = opt[:output]
52
+ digest = case trigger
53
+ when /-days?$/
54
+ :day
55
+ when /-entries$/
56
+ :entries
57
+ else
58
+ :digest
59
+ end
60
+
61
+ all_items = []
62
+ days = {}
63
+ flagged = false
64
+ tags = []
65
+
66
+ items.each do |i|
67
+ day_flagged = false
68
+ date_key = i.date.strftime('%Y-%m-%d')
69
+
70
+ if String.method_defined? :force_encoding
71
+ title = i.title.force_encoding('utf-8').link_urls(format: :markdown)
72
+ note = i.note.map { |line| line.force_encoding('utf-8').strip.link_urls(format: :markdown) } if i.note
73
+ else
74
+ title = i.title.link_urls(format: :markdown)
75
+ note = i.note.map { |line| line.strip.link_urls(format: :markdown) } if i.note
76
+ end
77
+
78
+ title = "#{title} @project(#{i.section})" unless variables[:is_single]
79
+
80
+ tags.concat(i.tag_array).sort!.uniq!
81
+ flagged = day_flagged = true if i.tags?(wwid.config['marker_tag'])
82
+
83
+ interval = wwid.get_interval(i, record: true) if i.title =~ /@done\((\d{4}-\d\d-\d\d \d\d:\d\d.*?)\)/ && opt[:times]
84
+ interval ||= false
85
+ human_time = false
86
+ if interval
87
+ d, h, m = wwid.format_time(wwid.get_interval(i, formatted: false))
88
+ human_times = []
89
+ human_times << format('%<d>d day%<p>s', d: d, p: d == 1 ? '' : 's') if d > 0
90
+ human_times << format('%<h>d hour%<p>s', h: h, p: h == 1 ? '' : 's') if h > 0
91
+ human_times << format('%<m>d minute%<p>s', m: m, p: m == 1 ? '' : 's') if m > 0
92
+ human_time = human_times.join(', ')
93
+ end
94
+
95
+ done = i.tags?('done') ? ' ' : ' '
96
+
97
+ item = {
98
+ date_object: i.date,
99
+ date: i.date.strftime('%a %-I:%M%p'),
100
+ shortdate: i.date.relative_date,
101
+ done: done,
102
+ note: note,
103
+ section: i.section,
104
+ time: interval,
105
+ human_time: human_time,
106
+ title: title.strip,
107
+ starred: day_flagged,
108
+ tags: i.tag_array
109
+ }
110
+ all_items << item
111
+
112
+
113
+ if days.key?(date_key)
114
+ days[date_key][:starred] = true if day_flagged
115
+ days[date_key][:tags] = days[date_key][:tags].concat(i.tag_array).sort.uniq
116
+ days[date_key][:entries].push(item)
117
+ else
118
+ days[date_key] ||= { tags: [], entries: [], starred: false }
119
+ days[date_key][:starred] = true if day_flagged
120
+ days[date_key][:tags] = days[date_key][:tags].concat(i.tag_array).sort.uniq
121
+ days[date_key][:entries].push(item)
122
+ end
123
+ end
124
+
125
+
126
+ template = if wwid.config['export_templates']['dayone'] && File.exist?(File.expand_path(wwid.config['export_templates']['dayone']))
127
+ IO.read(File.expand_path(wwid.config['export_templates']['dayone']))
128
+ else
129
+ self.template('dayone')
130
+ end
131
+
132
+ totals = opt[:totals] ? wwid.tag_times(format: :markdown, sort_by_name: opt[:sort_tags], sort_order: opt[:tag_order]) : ''
133
+
134
+ case digest
135
+ when :day
136
+ days.each do |k, hsh|
137
+ title = "#{k}: #{variables[:page_title]}"
138
+ to_dayone(template: template,
139
+ title: title,
140
+ items: hsh[:entries],
141
+ totals: '',
142
+ date: Time.parse(k),
143
+ tags: tags,
144
+ starred: hsh[:starred])
145
+ end
146
+ when :entries
147
+ entry_template = if wwid.config['export_templates']['dayone_entry'] && File.exist?(File.expand_path(wwid.config['export_templates']['dayone_entry']))
148
+ IO.read(File.expand_path(wwid.config['export_templates']['dayone_entry']))
149
+ else
150
+ self.template('dayone-entry')
151
+ end
152
+ all_items.each do |item|
153
+ to_dayone(template: entry_template,
154
+ title: '',
155
+ items: [item],
156
+ totals: '',
157
+ date: item[:date_object],
158
+ tags: item[:tags],
159
+ starred: item[:starred])
160
+ end
161
+ else
162
+ to_dayone(template: template,
163
+ title: variables[:page_title],
164
+ items: all_items,
165
+ totals: totals,
166
+ date: Time.now,
167
+ tags: tags,
168
+ starred: flagged)
169
+ end
170
+
171
+ @out = ''
172
+ end
173
+
174
+ def self.to_dayone(template: self.template(nil), title: 'doing', items: [], totals: '', date: Time.now, tags: [], starred: false)
175
+ mdx = DayOneRenderer.new(title, items, totals)
176
+
177
+ engine = ERB.new(template)
178
+ content = engine.result(mdx.get_binding)
179
+
180
+ uuid = SecureRandom.uuid
181
+ # uuid = `uuidgen`.strip
182
+
183
+ plist = {
184
+ 'Creation Date' => date,
185
+ 'Creator' => { 'Software Agent' => 'Doing/2.0.0' },
186
+ 'Entry Text' => content,
187
+ 'Starred' => starred,
188
+ 'Tags' => tags.sort.uniq.delete_if { |t| t =~ /(done|cancell?ed|from)/ },
189
+ 'UUID' => uuid
190
+ }
191
+
192
+ container = File.expand_path('~/Library/Group Containers/')
193
+ dayone_dir = Dir.glob('*.dayoneapp2', base: container).first
194
+ import_dir = File.join(container, dayone_dir, 'Data', 'Auto Import', 'Default Journal.dayone', 'entries')
195
+ FileUtils.mkdir_p(import_dir) unless File.exist?(import_dir)
196
+ entry_file = File.join(import_dir, "#{uuid}.doentry")
197
+ Doing.logger.debug('Day One Export:', "Exporting to #{entry_file}")
198
+ File.open(entry_file, 'w') do |f|
199
+ f.puts plist.to_plist
200
+ end
201
+
202
+ Doing.logger.count(:exported, level: :info, count: items.count, message: '%count %items exported to Day One import folder')
203
+ end
204
+
205
+ Doing::Plugins.register 'dayone', :export, self
206
+ Doing::Plugins.register 'dayone-days', :export, self
207
+ Doing::Plugins.register 'dayone-entries', :export, self
208
+ end
209
+ end