todo-txt 0.9 → 0.10

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 04e209879cbf8434759e4343c03bdbb2b48a7e3f
4
- data.tar.gz: bad9b8396a6d32ef8eb8f647394de850d5c0a658
3
+ metadata.gz: 0e6194a6c093149543776903fec449488ff35e12
4
+ data.tar.gz: 1ff563eca42c38f95a55eda4732f4c43005a2079
5
5
  SHA512:
6
- metadata.gz: 7d5599684ab23946adde905819a231f75d38029780c49e0771acd0258a6622fd88d5edd1c0e5ff2d6415b1e359047b7b6fbef8cd684c701ce94fdafa972d2b31
7
- data.tar.gz: 9b7c601dad7b1a07541fea804b06a9c12490406501a7173b4e2e0dfaf26d5da320a7605960f4e8b9d5ba5f66d10dac3892c50bee9e2676d95fbb8df295f7c014
6
+ metadata.gz: 0d614c4914dfc5e7e84addd6185f79c52270a4196003be1bef3f88452063e4d549062855d3229b92413dfe1f9b0f896ff6fe12cc7b64786767300aa9ff430a56
7
+ data.tar.gz: 0b8307a2946e632a5a68fb1b03b1df5d2fcbf56317087ebac52e7d3317b742ba8e9a5f7e31de9ff8723683e940a50d2d11bd39e58a4b806265a05180f6fa123e
data/.rubocop.yml CHANGED
@@ -1,2 +1,5 @@
1
+ AllCops:
2
+ Exclude:
3
+ - 'lib/todo-txt.rb'
1
4
  StringLiterals:
2
5
  Enabled: false
data/.travis.yml CHANGED
@@ -1,6 +1,5 @@
1
1
  language: ruby
2
2
  rvm:
3
- - 2.0
4
3
  - 2.1
5
4
  - 2.2
6
5
  - 2.3.0
data/Gemfile CHANGED
@@ -4,7 +4,7 @@ group :test do
4
4
  gem 'rspec', '~> 3.4'
5
5
  gem 'rake'
6
6
  gem 'timecop'
7
- gem 'simplecov', :require => false
7
+ gem 'simplecov', require: false
8
8
  end
9
9
 
10
10
  group :development do
data/Rakefile CHANGED
@@ -2,7 +2,7 @@ require 'bundler/setup'
2
2
  require 'bundler/gem_tasks'
3
3
  require 'rspec/core/rake_task'
4
4
 
5
- task :default => [:test]
5
+ task default: :test
6
6
 
7
7
  desc "Run all tests"
8
8
  RSpec::Core::RakeTask.new(:test)
data/lib/todo-txt.rb CHANGED
@@ -1,6 +1 @@
1
- require 'logger'
2
- require 'todo-txt/logger'
3
- require 'todo-txt/options'
4
- require 'todo-txt/syntax'
5
- require 'todo-txt/list'
6
- require 'todo-txt/task'
1
+ require_relative 'todo'
data/lib/todo.rb ADDED
@@ -0,0 +1,38 @@
1
+ require 'logger'
2
+ require 'todo/logger'
3
+ require 'todo/options'
4
+ require 'todo/syntax'
5
+ require 'todo/list'
6
+ require 'todo/task'
7
+ require 'todo/file'
8
+
9
+ # Allows for easy management of task lists and tasks in the todo.txt format.
10
+ module Todo
11
+ # Provides global options for customizing todo list behaviour.
12
+ class << self
13
+ attr_accessor :options_instance
14
+ end
15
+
16
+ # Global access to custom options.
17
+ #
18
+ # Example:
19
+ #
20
+ # if Todo.options.require_completed_on
21
+ # # Do something
22
+ # end
23
+ def self.options
24
+ self.options_instance ||= Options.new
25
+ end
26
+
27
+ # Customize global list behaviour.
28
+ #
29
+ # Example:
30
+ #
31
+ # Todo.customize do |opts|
32
+ # opts.require_completed_on = false
33
+ # end
34
+ def self.customize
35
+ self.options_instance ||= Options.new
36
+ yield(options_instance)
37
+ end
38
+ end
data/lib/todo/file.rb ADDED
@@ -0,0 +1,83 @@
1
+ module Todo
2
+ # A high level wrapper around the Ruby File interface which supports reading
3
+ # from and writing to an IO handle with Todo::Task objects.
4
+ class File
5
+ # Open a list file handle and pass it to the given block. The file is
6
+ # automatically closed when the block returns.
7
+ #
8
+ # The file is opened in read-only mode by default.
9
+ #
10
+ # Todo::File.open("~/Dropbox/todo/todo.txt") do |file|
11
+ # file.each_task do |task|
12
+ # puts task.done?
13
+ # end
14
+ # end
15
+ #
16
+ # @param [String, Pathname] path
17
+ # @param [String] mode
18
+ def self.open(path, mode = 'r')
19
+ ios = new(path, mode)
20
+
21
+ if block_given?
22
+ yield ios
23
+ return ios.close
24
+ end
25
+
26
+ ios
27
+ end
28
+
29
+ # @param [String, Pathname] path
30
+ def self.read(path)
31
+ list = []
32
+
33
+ open(path) do |file|
34
+ file.each_task do |task|
35
+ list << task
36
+ end
37
+ end
38
+
39
+ list
40
+ end
41
+
42
+ # @param [String, Pathname] path
43
+ # @param [Array, Todo::List] list
44
+ def self.write(path, list)
45
+ open(path, 'w') do |file|
46
+ list.each do |task|
47
+ file.puts(task)
48
+ end
49
+ end
50
+ end
51
+
52
+ # @param [String, Pathname] path
53
+ # @param [String] mode
54
+ def initialize(path, mode = 'r')
55
+ @ios = ::File.open(path, mode)
56
+ end
57
+
58
+ # Executes the block for every task in the list.
59
+ def each
60
+ return enum_for(:each) unless block_given?
61
+
62
+ @ios.each_line do |line|
63
+ yield Task.new(line)
64
+ end
65
+ end
66
+
67
+ alias each_task each
68
+
69
+ # Writes the given tasks to the underlying IO handle.
70
+ #
71
+ # @overload puts(task, ...)
72
+ # @param [Todo::Task] task
73
+ # @param [Todo::Task] ...
74
+ def puts(*tasks)
75
+ @ios.puts(tasks.map(&:to_s))
76
+ end
77
+
78
+ # Closes the IO handle and flushes any pending writes.
79
+ def close
80
+ @ios.close
81
+ end
82
+ end
83
+ end
data/lib/todo/list.rb ADDED
@@ -0,0 +1,127 @@
1
+ module Todo
2
+ # Initializes a Todo List object with a path to the corresponding todo.txt
3
+ # file. For example, if your todo.txt file is located at:
4
+ #
5
+ # /home/sam/Dropbox/todo/todo.txt
6
+ #
7
+ # You would initialize the list object like:
8
+ #
9
+ # list = Todo::List.new("/home/sam/Dropbox/todo/todo.txt")
10
+ #
11
+ # Alternately, you can initialize the object with an array of strings or
12
+ # tasks. If the array is of strings, the strings will be converted into
13
+ # tasks. You can supply a mixed list of string and tasks if you wish.
14
+ #
15
+ # Example:
16
+ #
17
+ # tasks = []
18
+ # tasks << "A task line"
19
+ # tasks << Todo::Task.new("A task object")
20
+ #
21
+ # list = Todo::List.new(tasks)
22
+ class List < Array
23
+ def initialize(list)
24
+ case list
25
+ when Array
26
+ tasks = list.map do |item|
27
+ case item
28
+ when String then Task.new(item)
29
+ when Task then item
30
+ else
31
+ raise "Cannot add #{item.class} to list."
32
+ end
33
+ end
34
+
35
+ concat(tasks)
36
+
37
+ when String
38
+ @path = list
39
+ concat(File.read(list))
40
+ end
41
+ end
42
+
43
+ # The path to the todo.txt file that you supplied when you created the
44
+ # Todo::List object.
45
+ attr_reader :path
46
+
47
+ # Filters the list by priority and returns a new list.
48
+ #
49
+ # Example:
50
+ #
51
+ # list = Todo::List.new("/path/to/list")
52
+ # list.by_priority("A")
53
+ # # => Will be a new list with only priority 'A' tasks
54
+ #
55
+ # @param priority [String]
56
+ # @return [Todo::List]
57
+ def by_priority(priority)
58
+ List.new(select { |task| task.priority == priority })
59
+ end
60
+
61
+ # Filters the list by context and returns a new list.
62
+ #
63
+ # Example:
64
+ #
65
+ # list = Todo::List.new("/path/to/list")
66
+ # list.by_context("@admin")
67
+ # # => <Todo::List> filtered by '@admin'
68
+ #
69
+ # @param context [String]
70
+ # @return [Todo::List]
71
+ def by_context(context)
72
+ List.new(select { |task| task.contexts.include? context })
73
+ end
74
+
75
+ # Filters the list by project and returns a new list.
76
+ #
77
+ # Example:
78
+ #
79
+ # list = Todo::List.new("/path/to/list")
80
+ # list.by_project("+blog")
81
+ # # => <Todo::List> filtered by '+blog'
82
+ #
83
+ # @param project [String]
84
+ # @return [Todo::List]
85
+ def by_project(project)
86
+ List.new(select { |task| task.projects.include? project })
87
+ end
88
+
89
+ # Filters the list by completed tasks and returns a new list.
90
+ #
91
+ # Example:
92
+ #
93
+ # list = Todo::List.new("/path/to/list")
94
+ # list.by_done
95
+ # # => <Todo::List> filtered by tasks that are done
96
+ #
97
+ # @return [Todo::List]
98
+ def by_done
99
+ List.new(select(&:done?))
100
+ end
101
+
102
+ # Filters the list by incomplete tasks and returns a new list.
103
+ #
104
+ # Example:
105
+ #
106
+ # list = Todo::List.new("/path/to/list")
107
+ # list.by_not_done
108
+ # # => <Todo::List> filtered by tasks that are not done
109
+ #
110
+ # @return [Todo::List]
111
+ def by_not_done
112
+ List.new(select { |task| task.done? == false })
113
+ end
114
+
115
+ # Saves the list to the original file location.
116
+ #
117
+ # Warning: This is a destructive operation and will overwrite any existing
118
+ # content in the file. It does not attempt to diff and append changes.
119
+ #
120
+ # If no `path` is specified in the constructor then an error is raised.
121
+ def save!
122
+ raise "No path specified." unless path
123
+
124
+ File.write(path, self)
125
+ end
126
+ end
127
+ end
File without changes
@@ -1,17 +1,4 @@
1
1
  module Todo
2
- class << self
3
- attr_accessor :options_instance
4
- end
5
-
6
- def self.options
7
- self.options_instance ||= Options.new
8
- end
9
-
10
- def self.customize
11
- self.options_instance ||= Options.new
12
- yield(options_instance)
13
- end
14
-
15
2
  # Options for default preferences and library settings that can be customized
16
3
  # by clients of the gem.
17
4
  class Options
@@ -1,5 +1,5 @@
1
1
  module Todo
2
- # Parsing and extraction of the core syntax fragments in todo.txt lines.
2
+ # Supports parsing and extracting core syntax fragments from todo.txt lines.
3
3
  module Syntax
4
4
  # The regex used to match contexts.
5
5
  CONTEXTS_PATTERN = /(?:\s+|^)@[^\s]+/
@@ -19,6 +19,9 @@ module Todo
19
19
  # The regex used to match due date.
20
20
  DUE_ON_PATTERN = /(?:due:)(\d{4}-\d{2}-\d{2})(?:\s+|$)/i
21
21
 
22
+ # The regex used to match generic tags.
23
+ TAGS_PATTERN = /([a-z]+):([a-z0-9_-]+)/i
24
+
22
25
  # Extracts the readable text content of a task line, stripping out all the
23
26
  # discrete pieces of metadata (priority, dates, completion flag, projects,
24
27
  # contexts, etc).
@@ -76,8 +79,8 @@ module Todo
76
79
 
77
80
  # Checks whether the given todo item has a completion flag set.
78
81
  #
79
- # This provides support for ad-hoc handwritten lists where the completed flag
80
- # is set but there is no completed date.
82
+ # This provides support for ad-hoc handwritten lists where the completed
83
+ # flag is set but there is no completed date.
81
84
  #
82
85
  # @param line [String] the todo item to be processed
83
86
  # @return [Boolean]
@@ -85,18 +88,13 @@ module Todo
85
88
  line[0] == COMPLETED_FLAG && line[1] == SINGLE_SPACE
86
89
  end
87
90
 
88
- # Extracts the completion date for the given todo item.
89
- # Returns nil if a valid date is not found.
91
+ # Extracts the collection of tag annotations for the given todo item.
92
+ # Returns an empty hash if no tags are found.
90
93
  #
91
94
  # @param line [String] the todo item to be processed
92
- # @return [Date] the due date of the line
93
- def extract_due_on_date(line)
94
- date = DUE_ON_PATTERN.match(line)
95
- begin
96
- Date.parse(date[1]) if date
97
- rescue ArgumentError
98
- return nil # The given date is not valid
99
- end
95
+ # @return [Hash<Symbol,String>] the collection of tag annotations
96
+ def extract_tags(line)
97
+ line.scan(TAGS_PATTERN).map { |tag, val| [tag.downcase.to_sym, val] }.to_h
100
98
  end
101
99
 
102
100
  # Extract the list of `@context` tags out of the task line.
@@ -12,36 +12,37 @@ module Todo
12
12
  include Todo::Logger
13
13
  include Todo::Syntax
14
14
 
15
- def initialize(task)
16
- @orig = task
17
- @priority = extract_priority(orig)
18
- @created_on = extract_created_on(orig)
19
- @due_on = extract_due_on_date(orig)
20
- @contexts ||= extract_contexts(orig)
21
- @projects ||= extract_projects(orig)
15
+ def initialize(line)
16
+ @raw = line
17
+ @priority = extract_priority(raw)
18
+ @created_on = extract_created_on(raw)
19
+ @tags = extract_tags(raw)
20
+ @contexts ||= extract_contexts(raw)
21
+ @projects ||= extract_projects(raw)
22
22
 
23
23
  if Todo.options.require_completed_on
24
- @completed_on = extract_completed_date(orig)
24
+ @completed_on = extract_completed_date(raw)
25
25
  @is_completed = !@completed_on.nil?
26
26
  else
27
- @completed_on = extract_completed_date(orig)
28
- @is_completed = check_completed_flag(orig)
27
+ @completed_on = extract_completed_date(raw)
28
+ @is_completed = check_completed_flag(raw)
29
29
  end
30
30
  end
31
31
 
32
- # Returns the original content of the task.
32
+ # Returns the raw content of the original task line.
33
33
  #
34
34
  # Example:
35
35
  #
36
- # task = Todo::Task.new "(A) @context +project Hello!"
37
- # task.orig #=> "(A) @context +project Hello!"
38
- attr_reader :orig
36
+ # task = Todo::Task.new("(A) @context +project Hello!")
37
+ # task.raw
38
+ # # => "(A) @context +project Hello!"
39
+ attr_reader :raw
39
40
 
40
41
  # Returns the task's creation date, if any.
41
42
  #
42
43
  # Example:
43
44
  #
44
- # task = Todo::Task.new "(A) 2012-03-04 Task."
45
+ # task = Todo::Task.new("(A) 2012-03-04 Task.")
45
46
  # task.created_on
46
47
  # #=> <Date: 2012-03-04 (4911981/2,0,2299161)>
47
48
  #
@@ -54,53 +55,57 @@ module Todo
54
55
  #
55
56
  # Example:
56
57
  #
57
- # task = Todo::Task.new "x 2012-03-04 Task."
58
+ # task = Todo::Task.new("x 2012-03-04 Task.")
58
59
  # task.completed_on
59
- # #=> <Date: 2012-03-04 (4911981/2,0,2299161)>
60
+ # # => <Date: 2012-03-04 (4911981/2,0,2299161)>
60
61
  #
61
62
  # Dates _must_ be in the YYYY-MM-DD format as specified in the todo.txt
62
63
  # format. Dates in any other format will be classed as malformed and this
63
64
  # attribute will be nil.
64
65
  attr_reader :completed_on
65
66
 
66
- # Returns the task's due date, if any.
67
+ # Returns tag annotations embedded in the list item.
67
68
  #
68
69
  # Example:
69
70
  #
70
- # task = Todo::Task.new "(A) This is a task. due:2012-03-04"
71
- # task.due_on
72
- # #=> <Date: 2012-03-04 (4911981/2,0,2299161)>
71
+ # task = Todo::Task.new("Some task. due:2016-06-16 hello:world")
72
+ # task.tags
73
+ # # => { :due => '2016-06-16', :hello => 'world' }
73
74
  #
74
- # Dates _must_ be in the YYYY-MM-DD format as specified in the todo.txt
75
- # format. Dates in any other format will be classed as malformed and this
76
- # attribute will be nil.
77
- attr_reader :due_on
75
+ # task = Todo::Task.new("Some task.")
76
+ # task.tags.empty?
77
+ # # => true
78
+ attr_reader :tags
78
79
 
79
80
  # Returns the priority, if any.
80
81
  #
81
82
  # Example:
82
83
  #
83
- # task = Todo::Task.new "(A) Some task."
84
- # task.priority #=> "A"
84
+ # task = Todo::Task.new("(A) Some task.")
85
+ # task.priority
86
+ # # => "A"
85
87
  #
86
88
  # task = Todo::Task.new "Some task."
87
- # task.priority #=> nil
89
+ # task.priority
90
+ # # => nil
88
91
  attr_reader :priority
89
92
 
90
93
  # Returns an array of all the @context annotations.
91
94
  #
92
95
  # Example:
93
96
  #
94
- # task = Todo:Task.new "(A) @context Testing!"
95
- # task.context #=> ["@context"]
97
+ # task = Todo:Task.new("(A) @context Testing!")
98
+ # task.context
99
+ # # => ["@context"]
96
100
  attr_reader :contexts
97
101
 
98
102
  # Returns an array of all the +project annotations.
99
103
  #
100
104
  # Example:
101
105
  #
102
- # task = Todo:Task.new "(A) +test Testing!"
103
- # task.projects #=> ["+test"]
106
+ # task = Todo:Task.new("(A) +test Testing!")
107
+ # task.projects
108
+ # # => ["+test"]
104
109
  attr_reader :projects
105
110
 
106
111
  # Gets just the text content of the todo, without the priority, contexts
@@ -108,29 +113,44 @@ module Todo
108
113
  #
109
114
  # Example:
110
115
  #
111
- # task = Todo::Task.new "(A) @test Testing!"
112
- # task.text #=> "Testing!"
116
+ # task = Todo::Task.new("(A) @test Testing!")
117
+ # task.text
118
+ # # => "Testing!"
113
119
  def text
114
- @text ||= extract_item_text(orig)
120
+ @text ||= extract_item_text(raw)
115
121
  end
116
122
 
117
- # Returns the task's creation date, if any.
123
+ # Deprecated. See: #created_on
124
+ def date
125
+ logger.warn("`Task#date` is deprecated, use `Task#created_on` instead.")
126
+
127
+ @created_on
128
+ end
129
+
130
+ # Deprecated. See: #raw
131
+ def orig
132
+ logger.warn("`Task#orig` is deprecated, use `Task#raw` instead.")
133
+
134
+ raw
135
+ end
136
+
137
+ # Returns the task's due date, if any.
118
138
  #
119
139
  # Example:
120
140
  #
121
- # task = Todo::Task.new "(A) 2012-03-04 Task."
122
- # task.date
123
- # #=> <Date: 2012-03-04 (4911981/2,0,2299161)>
141
+ # task = Todo::Task.new("(A) This is a task. due:2012-03-04")
142
+ # task.due_on
143
+ # # => <Date: 2012-03-04 (4911981/2,0,2299161)>
124
144
  #
125
145
  # Dates _must_ be in the YYYY-MM-DD format as specified in the todo.txt
126
146
  # format. Dates in any other format will be classed as malformed and this
127
- # method will return nil.
128
- #
129
- # Deprecated
130
- def date
131
- logger.warn("Task#date is deprecated, use created_on instead.")
132
-
133
- @created_on
147
+ # attribute will be nil.
148
+ def due_on
149
+ begin
150
+ Date.parse(tags[:due]) if tags[:due] =~ /(\d{4}-\d{2}-\d{2})/
151
+ rescue ArgumentError
152
+ return nil
153
+ end
134
154
  end
135
155
 
136
156
  # Returns whether a task's due date is in the past.
@@ -139,22 +159,22 @@ module Todo
139
159
  #
140
160
  # task = Todo::Task.new("This task is overdue! due:#{Date.today - 1}")
141
161
  # task.overdue?
142
- # #=> true
162
+ # # => true
143
163
  def overdue?
144
164
  !due_on.nil? && due_on < Date.today
145
165
  end
146
166
 
147
- # Returns if the task is done.
167
+ # Returns true if the task is completed.
148
168
  #
149
169
  # Example:
150
170
  #
151
- # task = Todo::Task.new "x 2012-12-08 Task."
171
+ # task = Todo::Task.new("x 2012-12-08 Task.")
152
172
  # task.done?
153
- # #=> true
173
+ # # => true
154
174
  #
155
- # task = Todo::Task.new "Task."
175
+ # task = Todo::Task.new("Task.")
156
176
  # task.done?
157
- # #=> false
177
+ # # => false
158
178
  def done?
159
179
  @is_completed
160
180
  end
@@ -163,17 +183,16 @@ module Todo
163
183
  #
164
184
  # Example:
165
185
  #
166
- # task = Todo::Task.new "2012-12-08 Task."
186
+ # task = Todo::Task.new("2012-12-08 Task.")
187
+ #
167
188
  # task.done?
168
- # #=> false
189
+ # # => false
169
190
  #
191
+ # # Complete the task
170
192
  # task.do!
193
+ #
171
194
  # task.done?
172
- # #=> true
173
- # task.created_on
174
- # #=> <Date: 2012-12-08 (4911981/2,0,2299161)>
175
- # task.completed_on
176
- # #=> # the current date
195
+ # # => true
177
196
  def do!
178
197
  @completed_on = Date.today
179
198
  @is_completed = true
@@ -184,21 +203,19 @@ module Todo
184
203
  #
185
204
  # Example:
186
205
  #
187
- # task = Todo::Task.new "x 2012-12-08 2012-03-04 Task."
206
+ # task = Todo::Task.new("x 2012-12-08 2012-03-04 Task.")
188
207
  # task.done?
189
- # #=> true
208
+ # # => true
190
209
  #
210
+ # # Undo the completed task
191
211
  # task.undo!
212
+ #
192
213
  # task.done?
193
- # #=> false
194
- # task.created_on
195
- # #=> <Date: 2012-03-04 (4911981/2,0,2299161)>
196
- # task.completed_on
197
- # #=> nil
214
+ # # => false
198
215
  def undo!
199
216
  @completed_on = nil
200
217
  @is_completed = false
201
- @priority = extract_priority(orig)
218
+ @priority = extract_priority(raw)
202
219
  end
203
220
 
204
221
  # Increases the priority until A. If it's nil, it sets it to A.
@@ -225,17 +242,20 @@ module Todo
225
242
  #
226
243
  # Example:
227
244
  #
228
- # task = Todo::Task.new "x 2012-12-08 Task."
245
+ # task = Todo::Task.new("x 2012-12-08 Task.")
229
246
  # task.done?
230
- # #=> true
247
+ # # => true
231
248
  #
249
+ # # Toggle between complete and incomplete
232
250
  # task.toggle!
251
+ #
233
252
  # task.done?
234
- # #=> false
253
+ # # => false
235
254
  #
236
255
  # task.toggle!
256
+ #
237
257
  # task.done?
238
- # #=> true
258
+ # # => true
239
259
  def toggle!
240
260
  done? ? undo! : do!
241
261
  end
@@ -244,25 +264,27 @@ module Todo
244
264
  #
245
265
  # Example:
246
266
  #
247
- # task = Todo::Task.new "(A) 2012-12-08 Task"
267
+ # task = Todo::Task.new("(A) 2012-12-08 Task")
248
268
  # task.to_s
249
- # #=> "(A) 2012-12-08 Task"
269
+ # # => "(A) 2012-12-08 Task"
250
270
  def to_s
251
- priority_string = priority ? "(#{priority}) " : ""
252
- done_string = done? ? "x #{completed_on} " : ""
253
- created_on_string = created_on ? "#{created_on} " : ""
254
- contexts_string = contexts.empty? ? "" : " #{contexts.join ' '}"
255
- projects_string = projects.empty? ? "" : " #{projects.join ' '}"
256
- due_on_string = due_on.nil? ? "" : " due:#{due_on}"
257
- "#{done_string}#{priority_string}#{created_on_string}#{text}#{contexts_string}#{projects_string}#{due_on_string}"
271
+ [
272
+ done? && "x #{completed_on}",
273
+ priority && "(#{priority})",
274
+ created_on.to_s,
275
+ text,
276
+ contexts.join(' '),
277
+ projects.join(' '),
278
+ tags.map { |tag,val| "#{tag}:#{val}" }.join(' ')
279
+ ].grep(String).join(' ').strip
258
280
  end
259
281
 
260
282
  # Compares the priorities of two tasks.
261
283
  #
262
284
  # Example:
263
285
  #
264
- # task1 = Todo::Task.new "(A) Priority A."
265
- # task2 = Todo::Task.new "(B) Priority B."
286
+ # task1 = Todo::Task.new("(A) Priority A.")
287
+ # task2 = Todo::Task.new("(B) Priority B.")
266
288
  #
267
289
  # task1 > task2
268
290
  # # => true
@@ -0,0 +1,75 @@
1
+ require 'spec_helper'
2
+
3
+ describe Todo::File do
4
+ let(:path) { File.dirname(__FILE__) + '/../data/todo.txt' }
5
+
6
+ context 'reading input' do
7
+ it 'should raise a system error when file not found' do
8
+ expect { Todo::File.open('not_found') }.to raise_error Errno::ENOENT
9
+ end
10
+
11
+ it 'should yield each task from block passed to open' do
12
+ Todo::File.open(path) do |file|
13
+ file.each do |task|
14
+ expect(task).to be_a Todo::Task
15
+ end
16
+ end
17
+ end
18
+
19
+ it 'should return a file object when no block is passed to open' do
20
+ file = Todo::File.open(path)
21
+ expect(file).to be_a Todo::File
22
+ end
23
+
24
+ it 'should return an enumerator when no block is passed to each' do
25
+ enumerator = Todo::File.open(path).each
26
+ expect(enumerator).to be_a Enumerator
27
+ expect(enumerator.next).to be_a Todo::Task
28
+ end
29
+
30
+ it 'should read a file into an array of tasks' do
31
+ list = Todo::File.read(path)
32
+ expect(list).to be_a Array
33
+ expect(list.first).to be_a Todo::Task
34
+ end
35
+ end
36
+
37
+ context 'writing' do
38
+ let(:path) { File.dirname(__FILE__) + '/../data/test.txt' }
39
+
40
+ it 'should write to a list file when initialized in writeable mode' do
41
+ file = Todo::File.new(path, 'w')
42
+ file.puts(Todo::Task.new('Task 1'))
43
+ file.puts(Todo::Task.new('Task 2'))
44
+ file.close
45
+
46
+ target = File.open(path)
47
+ expect(target.gets.rstrip).to eq("Task 1")
48
+ expect(target.gets.rstrip).to eq("Task 2")
49
+ end
50
+
51
+ it 'should write to a list file when opened in writeable mode' do
52
+ Todo::File.open(path, 'w') do |file|
53
+ file.puts(Todo::Task.new('Task 1'))
54
+ file.puts(Todo::Task.new('Task 2'))
55
+ end
56
+
57
+ target = File.open(path)
58
+ expect(target.gets.rstrip).to eq("Task 1")
59
+ expect(target.gets.rstrip).to eq("Task 2")
60
+ end
61
+
62
+ it 'should write each task in the list to a line in the file' do
63
+ list = Todo::List.new([Todo::Task.new('Task 1'), Todo::Task.new('Task 2')])
64
+ Todo::File.write(path, list)
65
+
66
+ target = File.open(path)
67
+ expect(target.gets.rstrip).to eq("Task 1")
68
+ expect(target.gets.rstrip).to eq("Task 2")
69
+ end
70
+
71
+ after do
72
+ File.delete(path)
73
+ end
74
+ end
75
+ end
@@ -7,17 +7,17 @@ describe Todo::List do
7
7
 
8
8
  context 'create with an array' do
9
9
  it 'successfully creates an object' do
10
- array = Array.new
11
- array.push "(A) A string task!"
12
- array.push Todo::Task.new("(A) An actual task!")
13
- expect(Todo::List.new array).not_to eq(nil)
10
+ items = []
11
+ items << "(A) A string task!"
12
+ items << Todo::Task.new("(A) An actual task!")
13
+ expect(Todo::List.new(items).length).to eq(2)
14
14
  end
15
15
 
16
16
  it 'does not have a path' do
17
- array = Array.new
18
- array.push "(A) A string task!"
19
- array.push Todo::Task.new("(A) An actual task!")
20
- expect(Todo::List.new(array).path).to eq(nil)
17
+ items = []
18
+ items << "(A) A string task!"
19
+ items << Todo::Task.new("(A) An actual task!")
20
+ expect(Todo::List.new(items).path).to eq(nil)
21
21
  end
22
22
  end
23
23
 
@@ -38,12 +38,9 @@ describe Todo::List do
38
38
  end
39
39
 
40
40
  it 'accepts a mix of tasks and strings' do
41
- l = Todo::List.new([
42
- "A task!",
43
- Todo::Task.new("Another task!"),
44
- ])
41
+ mixed = Todo::List.new(["A task!", Todo::Task.new("Another task!")])
45
42
 
46
- expect(l.length).to eq(2)
43
+ expect(mixed.length).to eq(2)
47
44
  end
48
45
 
49
46
  it 'has the correct path' do
@@ -97,9 +94,7 @@ describe Todo::List do
97
94
  end
98
95
 
99
96
  it 'should be able to filter by project, context and priority' do
100
- filtered = list.by_project('+project').
101
- by_context('@context').
102
- by_priority('C')
97
+ filtered = list.by_project('+project').by_context('@context').by_priority('C')
103
98
 
104
99
  # Make sure some data was actually checked
105
100
  expect(filtered.length).to be > 0
@@ -115,17 +115,25 @@ describe Todo::Syntax do
115
115
  end
116
116
  end
117
117
 
118
- describe '#extract_due_on_date' do
118
+ describe '#extract_tags' do
119
119
  specify 'empty task' do
120
- expect(extract_due_on_date('')).to be nil
120
+ expect(extract_tags('')).to eq({})
121
121
  end
122
122
 
123
- specify 'task without due date' do
124
- expect(extract_due_on_date('something to do')).to be nil
123
+ specify 'task without tag' do
124
+ expect(extract_tags('something to do')).to be {}
125
125
  end
126
126
 
127
127
  specify 'task with due date' do
128
- expect(extract_due_on_date('something to do due:2016-03-30')).to eq(Date.new(2016, 03, 30))
128
+ expect(extract_tags('something to do due:2016-03-30')).to eq(:due => '2016-03-30')
129
+ end
130
+
131
+ specify 'task with case-insensitive due date' do
132
+ expect(extract_tags('something to do DUE:2016-03-30')).to eq(:due => '2016-03-30')
133
+ end
134
+
135
+ specify 'task with multiple tags' do
136
+ expect(extract_tags('something to do hello:world and foo:bar')).to eq(:hello => 'world', :foo => 'bar')
129
137
  end
130
138
  end
131
139
 
@@ -11,7 +11,7 @@ describe Todo::Task do
11
11
  task = Todo::Task.new '(A) 2012-12-08 My task @test +test2'
12
12
  Timecop.freeze(2013, 12, 8) do
13
13
  task.do!
14
- expect(task.orig).to eq('(A) 2012-12-08 My task @test +test2')
14
+ expect(task.raw).to eq('(A) 2012-12-08 My task @test +test2')
15
15
  end
16
16
  end
17
17
 
@@ -32,7 +32,7 @@ describe Todo::Task do
32
32
 
33
33
  it 'should retain the original task creation string' do
34
34
  task = Todo::Task.new '(A) This is an awesome task, yo. +winning'
35
- expect(task.orig).to eq('(A) This is an awesome task, yo. +winning')
35
+ expect(task.raw).to eq('(A) This is an awesome task, yo. +winning')
36
36
  end
37
37
 
38
38
  it 'should be able to get just the text, no contexts etc.' do
@@ -411,11 +411,22 @@ describe Todo::Task do
411
411
  logger = double(Logger)
412
412
  Todo::Logger.logger = logger
413
413
 
414
- task = Todo::Task.new 'x 2012-09-11 (B) 2012-03-04 This is a sweet task. @context due:2012-02-31 +project'
415
- error_message = 'Task#date is deprecated, use created_on instead.'
414
+ task = Todo::Task.new('x 2012-09-11 (B) 2012-03-04 This is a sweet task. @context due:2012-02-31 +project')
415
+ error_message = '`Task#date` is deprecated, use `Task#created_on` instead.'
416
416
 
417
417
  expect(logger).to receive(:warn).with(error_message)
418
418
  task.date
419
419
  end
420
+
421
+ it 'should call @logger.warn if #orig called as deprecated method' do
422
+ logger = double(Logger)
423
+ Todo::Logger.logger = logger
424
+
425
+ task = Todo::Task.new('x 2012-09-11 (B) 2012-03-04 This is a sweet task. @context due:2012-02-31 +project')
426
+ error_message = '`Task#orig` is deprecated, use `Task#raw` instead.'
427
+
428
+ expect(logger).to receive(:warn).with(error_message)
429
+ task.orig
430
+ end
420
431
  end
421
432
  end
data/todo-txt.gemspec CHANGED
@@ -1,11 +1,11 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = 'todo-txt'
3
- s.version = '0.9'
4
- s.authors = ["Sam Rose"]
5
- s.email = %q{samwho@lbak.co.uk}
6
- s.summary = %q{A client library for parsing todo.txt files.}
7
- s.homepage = %q{http://github.com/samwho/todo-txt-gem}
8
- s.description = %q{Allows for simple parsing of todo.txt files, as per Gina Trapani's todo.txt project.}
3
+ s.version = '0.10'
4
+ s.authors = ['Sam Rose']
5
+ s.email = 'samwho@lbak.co.uk'
6
+ s.summary = 'A client library for parsing todo.txt files.'
7
+ s.homepage = 'http://github.com/samwho/todo-txt-gem'
8
+ s.description = 'Allows for simple parsing of todo.txt files, as per Gina Trapani\'s todo.txt project.'
9
9
  s.required_ruby_version = '>= 2.0'
10
10
  s.license = 'GPL-2'
11
11
  s.files = `git ls-files`.split(/\n/)
data/todo.txt CHANGED
@@ -1,13 +1,19 @@
1
1
  x 2016-04-21 modify list#save! to work with windows line separators +bug
2
2
  determine correct/expected behaviour for missing file paths +feature +api
3
3
  dirty flag on lists that have modifications after loading +feature
4
- extract file handling into separate IO-like module +refactor +api
4
+ x 2016-04-27 extract file handling into separate IO-like module +refactor +api
5
5
  implement enumerable API on list rather than inherit from array +api
6
6
  list operations should be algebraically closed +api
7
7
  include rubocop in travis builds +build
8
8
  use << operation to move tasks between lists +api +feature
9
9
  create github issue with roadmap for 1.0 release +feature +api
10
10
  consider rewriting file write tests with mocking and stringio +refactor +build
11
- remove deprecated date method from task +api
11
+ remove deprecated date and orig methods from task +api
12
12
  pass in options as an instance rather than make it always global +refactor +api
13
13
  save a backup of an existing todo file when the IO object is read in +feature
14
+ x 2016-06-16 extract `due:` formatting into a more general syntax for adding tags +feature +refactor +api
15
+ x rename lib path to match standard gem conventions +refactor
16
+ x 2016-04-23 tidy up code examples in doc comments to match ruby styleguide conventions +refactor +docs
17
+ consider renaming or aliasing `#done?` to `#complete?` +api +refactor
18
+ add complementary method returning opposite of `#done?` to provide symmetry +api
19
+ restructure test specs to reflect different core concerns and areas of functionality +refactor
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: todo-txt
3
3
  version: !ruby/object:Gem::Version
4
- version: '0.9'
4
+ version: '0.10'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sam Rose
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-04-20 00:00:00.000000000 Z
11
+ date: 2016-07-02 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Allows for simple parsing of todo.txt files, as per Gina Trapani's todo.txt
14
14
  project.
@@ -26,14 +26,17 @@ files:
26
26
  - README.md
27
27
  - Rakefile
28
28
  - lib/todo-txt.rb
29
- - lib/todo-txt/list.rb
30
- - lib/todo-txt/logger.rb
31
- - lib/todo-txt/options.rb
32
- - lib/todo-txt/syntax.rb
33
- - lib/todo-txt/task.rb
29
+ - lib/todo.rb
30
+ - lib/todo/file.rb
31
+ - lib/todo/list.rb
32
+ - lib/todo/logger.rb
33
+ - lib/todo/options.rb
34
+ - lib/todo/syntax.rb
35
+ - lib/todo/task.rb
34
36
  - spec/data/tasks.txt
35
37
  - spec/data/todo.txt
36
38
  - spec/spec_helper.rb
39
+ - spec/todo-txt/file_spec.rb
37
40
  - spec/todo-txt/list_spec.rb
38
41
  - spec/todo-txt/options/require_completed_on_spec.rb
39
42
  - spec/todo-txt/options_spec.rb
@@ -61,9 +64,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
61
64
  version: '0'
62
65
  requirements: []
63
66
  rubyforge_project:
64
- rubygems_version: 2.2.2
67
+ rubygems_version: 2.4.5
65
68
  signing_key:
66
69
  specification_version: 4
67
70
  summary: A client library for parsing todo.txt files.
68
71
  test_files: []
69
- has_rdoc:
data/lib/todo-txt/list.rb DELETED
@@ -1,122 +0,0 @@
1
- module Todo
2
- # Initializes a Todo List object with a path to the corresponding todo.txt
3
- # file. For example, if your todo.txt file is located at:
4
- #
5
- # /home/sam/Dropbox/todo/todo.txt
6
- #
7
- # You would initialize this object like:
8
- #
9
- # list = Todo::List.new "/home/sam/Dropbox/todo/todo-txt"
10
- #
11
- # Alternately, you can initialize this object with an array of strings or
12
- # tasks. If the array is of strings, the strings will be converted into
13
- # tasks. You can supply a mixed list of string and tasks if you wish.
14
- #
15
- # Example:
16
- #
17
- # array = Array.new
18
- # array.push "(A) A string task!"
19
- # array.push Todo::Task.new("(A) An actual task!")
20
- #
21
- # list = Todo::List.new array
22
- class List < Array
23
- def initialize(list)
24
- if list.is_a? Array
25
- # No file path was given.
26
- @path = nil
27
-
28
- # If path is an array, loop over it, adding to self.
29
- list.each do |task|
30
- # If it's a string, make a new task out of it.
31
- if task.is_a? String
32
- push Task.new task
33
- # If it's a task, just add it.
34
- elsif task.is_a? Todo::Task
35
- push task
36
- end
37
- end
38
- elsif list.is_a? String
39
- @path = list
40
-
41
- # Read in lines from file, create Todo::Tasks out of them and push them
42
- # onto self.
43
- File.open(list) do |file|
44
- file.each_line { |line| push Task.new(line) }
45
- end
46
- end
47
- end
48
-
49
- # The path to the todo.txt file that you supplied when you created the
50
- # Todo::List object.
51
- attr_reader :path
52
-
53
- # Filters the list by priority and returns a new list.
54
- #
55
- # Example:
56
- #
57
- # list = Todo::List.new "/path/to/list"
58
- # list.by_priority "A" #=> Will be a new list with only priority A tasks
59
- def by_priority(priority)
60
- List.new(select { |task| task.priority == priority })
61
- end
62
-
63
- # Filters the list by context and returns a new list.
64
- #
65
- # Example:
66
- #
67
- # list = Todo::List.new "/path/to/list"
68
- # list.by_context "@context" #=> Will be a new list with only tasks
69
- # containing "@context"
70
- def by_context(context)
71
- List.new(select { |task| task.contexts.include? context })
72
- end
73
-
74
- # Filters the list by project and returns a new list.
75
- #
76
- # Example:
77
- #
78
- # list = Todo::List.new "/path/to/list"
79
- # list.by_project "+project" #=> Will be a new list with only tasks
80
- # containing "+project"
81
- def by_project(project)
82
- List.new(select { |task| task.projects.include? project })
83
- end
84
-
85
- # Filters the list by completed tasks and returns a new list.
86
- #
87
- # Example:
88
- #
89
- # list = Todo::List.new "/path/to/list"
90
- # list.by_done #=> Will be a new list with only tasks marked with
91
- # an [x]
92
- def by_done
93
- List.new(select(&:done?))
94
- end
95
-
96
- # Filters the list by incomplete tasks and returns a new list.
97
- #
98
- # Example:
99
- #
100
- # list = Todo::List.new "/path/to/list"
101
- # list.by_not_done #=> Will be a new list with only incomplete tasks
102
- def by_not_done
103
- List.new(select { |task| task.done? == false })
104
- end
105
-
106
- # Saves the list to the original file location.
107
- #
108
- # Warning: This is a destructive operation and will overwrite what is
109
- # currently there.
110
- #
111
- # If no `path` is specified in the constructor then an error is raised.
112
- def save!
113
- raise "No path specified." unless path
114
-
115
- File.open(path, 'w') do |outfile|
116
- each do |task|
117
- outfile.puts(task.to_s)
118
- end
119
- end
120
- end
121
- end
122
- end