klipbook 0.3.0 → 1.0.0

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 (54) hide show
  1. data/Gemfile +6 -5
  2. data/Gemfile.lock +62 -25
  3. data/Guardfile +1 -2
  4. data/README.md +66 -18
  5. data/Rakefile +5 -1
  6. data/bin/klipbook +85 -1
  7. data/example.png +0 -0
  8. data/features/collate.feature +51 -0
  9. data/features/fixtures/clippings-for-three-books.txt +105 -0
  10. data/features/list.feature +31 -0
  11. data/features/step_definitions/collate_steps.rb +61 -0
  12. data/features/step_definitions/list_steps.rb +15 -0
  13. data/features/support/env.rb +5 -1
  14. data/klipbook.gemspec +49 -32
  15. data/lib/klipbook/book.rb +18 -0
  16. data/lib/klipbook/clipping.rb +4 -10
  17. data/lib/klipbook/collator.rb +17 -0
  18. data/lib/klipbook/config.rb +22 -0
  19. data/lib/klipbook/fetcher.rb +29 -0
  20. data/lib/klipbook/invalid_source_error.rb +12 -0
  21. data/lib/klipbook/output/book_helpers.rb +12 -0
  22. data/lib/klipbook/{book_summary.erb → output/html_book_summary.erb} +65 -11
  23. data/lib/klipbook/output/html_summary_writer.rb +42 -0
  24. data/lib/klipbook/printer.rb +18 -0
  25. data/lib/klipbook/sources/amazon_site/book_scraper.rb +67 -0
  26. data/lib/klipbook/sources/amazon_site/scraper.rb +78 -0
  27. data/lib/klipbook/sources/kindle_device/entry.rb +11 -0
  28. data/lib/klipbook/sources/kindle_device/entry_parser.rb +85 -0
  29. data/lib/klipbook/sources/kindle_device/file.rb +57 -0
  30. data/lib/klipbook/sources/kindle_device/file_parser.rb +33 -0
  31. data/lib/klipbook/version.rb +1 -1
  32. data/lib/klipbook.rb +18 -5
  33. data/spec/lib/klipbook/book_spec.rb +33 -0
  34. data/spec/lib/klipbook/collator_spec.rb +40 -0
  35. data/spec/lib/klipbook/fetcher_spec.rb +81 -0
  36. data/spec/lib/klipbook/output/html_summary_writer_spec.rb +90 -0
  37. data/spec/lib/klipbook/printer_spec.rb +45 -0
  38. data/spec/lib/klipbook/sources/kindle_device/entry_parser_spec.rb +275 -0
  39. data/spec/lib/klipbook/sources/kindle_device/file_parser_spec.rb +68 -0
  40. data/spec/lib/klipbook/sources/kindle_device/file_spec.rb +163 -0
  41. metadata +158 -58
  42. data/features/list_books.feature +0 -23
  43. data/features/print_book_summary.feature +0 -10
  44. data/features/step_definitions/klipbook_steps.rb +0 -87
  45. data/lib/klipbook/book_summary.rb +0 -35
  46. data/lib/klipbook/cli.rb +0 -49
  47. data/lib/klipbook/clippings_file.rb +0 -50
  48. data/lib/klipbook/clippings_parser.rb +0 -98
  49. data/lib/klipbook/runner.rb +0 -29
  50. data/spec/lib/klipbook/book_summary_spec.rb +0 -30
  51. data/spec/lib/klipbook/clipping_spec.rb +0 -17
  52. data/spec/lib/klipbook/clippings_file_spec.rb +0 -60
  53. data/spec/lib/klipbook/clippings_parser_spec.rb +0 -367
  54. data/spec/lib/klipbook/runner_spec.rb +0 -87
@@ -0,0 +1,275 @@
1
+ require 'spec_helper'
2
+
3
+ describe Klipbook::Sources::KindleDevice::EntryParser do
4
+
5
+ describe '#build_entry' do
6
+ subject { Klipbook::Sources::KindleDevice::EntryParser.new.build_entry(entry_text) }
7
+
8
+ context 'passed an empty entry' do
9
+ let(:entry_text) { " " }
10
+
11
+ it { should be_nil }
12
+ end
13
+
14
+ context 'passed an incomplete entry' do
15
+ let(:entry_text) do
16
+ "Not long enough"
17
+ end
18
+
19
+ it { should be_nil }
20
+ end
21
+
22
+ context 'passed an entry with the title line "Book title (Author\'s Name)"' do
23
+ let (:entry_text) do
24
+ "Book title (Author's Name)\n" +
25
+ "- Highlight Loc. 466-69 | Added on Thursday, April 21, 2011, 07:31 AM\n" +
26
+ "\n" +
27
+ "Highlight text\n"
28
+ end
29
+
30
+ its(:title) { should == 'Book title' }
31
+
32
+ its(:author) { should == "Author's Name" }
33
+ end
34
+
35
+ context 'passed an entry with the title line "Book title (sub title) (Author\'s Name)"' do
36
+ let (:entry_text) do
37
+ "Book title (sub title) (Author's Name)\n" +
38
+ "- Highlight Loc. 466-69 | Added on Thursday, April 21, 2011, 07:31 AM\n" +
39
+ "\n" +
40
+ "Highlight text\n"
41
+ end
42
+
43
+ its(:title) { should == 'Book title (sub title)' }
44
+
45
+ its(:author) { should == "Author's Name" }
46
+ end
47
+
48
+ context 'passed an entry with the title line "Book title"' do
49
+ let (:entry_text) do
50
+ "Book title\n" +
51
+ "- Highlight Loc. 466-69 | Added on Thursday, April 21, 2011, 07:31 AM\n" +
52
+ "\n" +
53
+ "Highlight text\n"
54
+ end
55
+
56
+ its(:title) { should == 'Book title' }
57
+
58
+ its (:author) { should be_nil }
59
+ end
60
+
61
+ context 'passed an entry that is a highlight' do
62
+ let (:entry_text) do
63
+ "Book title\n" +
64
+ "- Highlight Loc. 466-69 | Added on Thursday, April 21, 2011, 07:31 AM\n" +
65
+ "\n" +
66
+ "The first line of the highlight\n" +
67
+ "The second line of the highlight"
68
+ end
69
+
70
+ its(:type) { should == :highlight }
71
+
72
+ it 'extracts the highlighted text' do
73
+ subject.text.should == "The first line of the highlight\nThe second line of the highlight"
74
+ end
75
+ end
76
+
77
+ context 'passed an entry that is a 4th-generation highlight' do
78
+ let (:entry_text) do
79
+ "Book title\n" +
80
+ "- Your Highlight Location 466-69 | Added on Thursday, April 21, 2011, 07:31 AM\n" +
81
+ "\n" +
82
+ "The first line of the highlight\n" +
83
+ "The second line of the highlight"
84
+ end
85
+
86
+ its(:type) { should == :highlight }
87
+
88
+ it 'extracts the highlighted text' do
89
+ subject.text.should == "The first line of the highlight\nThe second line of the highlight"
90
+ end
91
+ end
92
+
93
+ context 'passed an entry that is a note' do
94
+ let (:entry_text) do
95
+ "Book title\n" +
96
+ "- Note Loc. 623 | Added on Thursday, April 21, 2011, 07:31 AM\n" +
97
+ "\n" +
98
+ "The note text"
99
+ end
100
+
101
+ its(:type) { should == :note }
102
+
103
+ it 'extracts the note text' do
104
+ subject.text.should == "The note text"
105
+ end
106
+ end
107
+
108
+ context 'passed an entry that is a 4th-generation note' do
109
+ let (:entry_text) do
110
+ "Book title\n" +
111
+ "- Your Note Location 623 | Added on Thursday, April 21, 2011, 07:31 AM\n" +
112
+ "\n" +
113
+ "The note text"
114
+ end
115
+
116
+ its(:type) { should == :note }
117
+
118
+ it 'extracts the note text' do
119
+ subject.text.should == "The note text"
120
+ end
121
+ end
122
+
123
+ context 'passed an entry with a bookmark' do
124
+ let (:entry_text) do
125
+ "Book title\n" +
126
+ "- Bookmark on Page 1 | Loc. 406 | Added on Thursday, April 21, 2011, 07:31 AM\n" +
127
+ "\n" +
128
+ "\n"
129
+ end
130
+
131
+ its(:type) { should == :bookmark }
132
+
133
+ its(:text) { should == '' }
134
+ end
135
+
136
+ context 'passed an entry with a 4th-generation bookmark' do
137
+ let (:entry_text) do
138
+ "Book title\n" +
139
+ "- Your Bookmark on Page 1 | Location 406 | Added on Thursday, April 21, 2011, 07:31 AM\n" +
140
+ "\n" +
141
+ "\n"
142
+ end
143
+
144
+ its(:type) { should == :bookmark }
145
+
146
+ its(:text) { should == '' }
147
+ end
148
+
149
+ context 'passed an entry with a single location value' do
150
+ let (:entry_text) do
151
+ "Book title\n" +
152
+ "- Highlight Loc. 465 | Added on Thursday, April 21, 2011, 07:31 AM\n" +
153
+ "\n" +
154
+ "The first line of the highlight\n" +
155
+ "The second line of the highlight"
156
+ end
157
+
158
+ its(:location) { should == 465 }
159
+
160
+ its(:added_on) { should == DateTime.parse('Thursday, April 21, 2011, 07:31 AM') }
161
+ end
162
+
163
+ context 'passed a 4th-generation entry with a single location value' do
164
+ let (:entry_text) do
165
+ "Book title\n" +
166
+ "- Your Highlight Location 465 | Added on Thursday, April 21, 2011, 07:31 AM\n" +
167
+ "\n" +
168
+ "The first line of the highlight\n" +
169
+ "The second line of the highlight"
170
+ end
171
+
172
+ its(:location) { should == 465 }
173
+
174
+ its(:added_on) { should == DateTime.parse('Thursday, April 21, 2011, 07:31 AM') }
175
+ end
176
+
177
+ context 'passed an entry with a location range' do
178
+ let (:entry_text) do
179
+ "Book title\n" +
180
+ "- Highlight Loc. 466-69 | Added on Thursday, April 21, 2011, 07:31 AM\n" +
181
+ "\n" +
182
+ "The first line of the highlight\n" +
183
+ "The second line of the highlight"
184
+ end
185
+
186
+ it 'extracts the first element of the location range' do
187
+ subject.location.should == 466
188
+ end
189
+
190
+ its(:added_on) { should == DateTime.parse('Thursday, April 21, 2011, 07:31 AM') }
191
+ end
192
+
193
+ context 'passed a 4th-generation entry with a location range' do
194
+ let (:entry_text) do
195
+ "Book title\n" +
196
+ "- Your Highlight Location 466-69 | Added on Thursday, April 21, 2011, 07:31 AM\n" +
197
+ "\n" +
198
+ "The first line of the highlight\n" +
199
+ "The second line of the highlight"
200
+ end
201
+
202
+ it 'extracts the first element of the location range' do
203
+ subject.location.should == 466
204
+ end
205
+
206
+ its(:added_on) { should == DateTime.parse('Thursday, April 21, 2011, 07:31 AM') }
207
+ end
208
+
209
+ context 'passed an entry with a page number and location range' do
210
+ let (:entry_text) do
211
+ "Book title\n" +
212
+ "- Highlight on Page 171 | Loc. 1858-59 | Added on Thursday, April 21, 2011, 07:31 AM\n" +
213
+ "\n" +
214
+ "Some highlighted text\n" +
215
+ "\n"
216
+ end
217
+
218
+ it 'extracts the first element of the location range' do
219
+ subject.location.should == 1858
220
+ end
221
+
222
+ its(:page) { should == 171 }
223
+
224
+ its(:added_on) { should == DateTime.parse('Thursday, April 21, 2011, 07:31 AM') }
225
+ end
226
+
227
+ context 'passed a 4th-generation entry with a page number and location range' do
228
+ let (:entry_text) do
229
+ "Book title\n" +
230
+ "- Your Highlight on Page 171 | Location 1858-59 | Added on Thursday, April 21, 2011, 07:31 AM\n" +
231
+ "\n" +
232
+ "Some highlighted text\n" +
233
+ "\n"
234
+ end
235
+
236
+ it 'extracts the first element of the location range' do
237
+ subject.location.should == 1858
238
+ end
239
+
240
+ its(:page) { should == 171 }
241
+
242
+ its(:added_on) { should == DateTime.parse('Thursday, April 21, 2011, 07:31 AM') }
243
+ end
244
+
245
+ context 'passed an entry with a page number and no location' do
246
+ let (:entry_text) do
247
+ "Book title\n" +
248
+ "- Highlight on Page 9 | Added on Thursday, April 21, 2011, 07:31 AM\n" +
249
+ "\n" +
250
+ "Clipping"
251
+ end
252
+
253
+ its(:location) { should == 0 }
254
+
255
+ its(:page) { should == 9 }
256
+
257
+ its(:added_on) { should == DateTime.parse('Thursday, April 21, 2011, 07:31 AM') }
258
+ end
259
+
260
+ context 'passed a 4th-generation entry with a page number and no location' do
261
+ let (:entry_text) do
262
+ "Book title\n" +
263
+ "- Your Highlight on Page 9 | Added on Thursday, April 21, 2011, 07:31 AM\n" +
264
+ "\n" +
265
+ "Clipping"
266
+ end
267
+
268
+ its(:location) { should == 0 }
269
+
270
+ its(:page) { should == 9 }
271
+
272
+ its(:added_on) { should == DateTime.parse('Thursday, April 21, 2011, 07:31 AM') }
273
+ end
274
+ end
275
+ end
@@ -0,0 +1,68 @@
1
+ # encoding: UTF-8
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Klipbook::Sources::KindleDevice::FileParser do
6
+
7
+ let(:parser) { Klipbook::Sources::KindleDevice::FileParser.new(entry_parser) }
8
+
9
+ let(:entry_parser) do
10
+ mock_parser = Object.new
11
+ stub(mock_parser).build_entry
12
+ mock_parser
13
+ end
14
+
15
+ describe '#extract_entries' do
16
+ subject { parser.extract_entries(raw_text) }
17
+
18
+ context 'called with empty text' do
19
+
20
+ let(:raw_text) { '' }
21
+
22
+ it { should be_empty }
23
+ end
24
+
25
+ context 'called with text containing two entries' do
26
+ let(:raw_text) do
27
+ " entry one" +
28
+ "==========" +
29
+ " entry two" +
30
+ "=========="
31
+ end
32
+
33
+ it 'builds two entries with the entries parser and returns them' do
34
+ entry_one = Object.new
35
+ entry_two = Object.new
36
+
37
+ stub(entry_parser).build_entry(' entry one') { entry_one }
38
+ stub(entry_parser).build_entry(' entry two') { entry_two }
39
+
40
+ subject.should == [ entry_one, entry_two ]
41
+ end
42
+ end
43
+
44
+ context 'called with text containing carriage return characters' do
45
+ let(:raw_text) do
46
+ "Example \r text\r" +
47
+ "=========="
48
+ end
49
+
50
+ it 'strips carriage returns before calling the entry parser' do
51
+ subject
52
+ entry_parser.should have_received.build_entry('Example text')
53
+ end
54
+ end
55
+
56
+ context 'called with text containing control characters' do
57
+ let(:raw_text) do
58
+ "Example \xef\xbb\xbf text\xef\xbb\xbf" +
59
+ "=========="
60
+ end
61
+
62
+ it 'strips control characters before calling the entry parser' do
63
+ subject
64
+ entry_parser.should have_received.build_entry('Example text')
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,163 @@
1
+ require 'spec_helper'
2
+
3
+ describe Klipbook::Sources::KindleDevice::File do
4
+
5
+ let(:file) { Klipbook::Sources::KindleDevice::File.new('file text', max_books, file_parser) }
6
+
7
+ let (:max_books) { 30 }
8
+
9
+ let (:entries) { [] }
10
+
11
+ let (:file_parser) do
12
+ Object.new.tap do |fake_parser|
13
+ stub(fake_parser).extract_entries { entries }
14
+ end
15
+ end
16
+
17
+ describe '#books' do
18
+ subject { file.books }
19
+
20
+ it 'parses the file text with the file parser' do
21
+ subject
22
+ file_parser.should have_received.extract_entries('file text')
23
+ end
24
+
25
+ context 'with entries for three books' do
26
+ let(:entries) do
27
+ [
28
+ Klipbook::Sources::KindleDevice::Entry.new do |e|
29
+ e.title = 'Book one'
30
+ e.author = 'Author one'
31
+ e.type = :highlight
32
+ e.added_on = DateTime.new(2012, 10, 10)
33
+ end,
34
+ Klipbook::Sources::KindleDevice::Entry.new do |e|
35
+ e.title = 'Book one'
36
+ e.author = 'Author one'
37
+ e.type = :highlight
38
+ e.added_on = DateTime.new(2012, 10, 10)
39
+ end,
40
+ Klipbook::Sources::KindleDevice::Entry.new do |e|
41
+ e.title = 'Book two'
42
+ e.author = 'Author two'
43
+ e.type = :highlight
44
+ e.added_on = DateTime.new(2012, 10, 12)
45
+ end,
46
+ Klipbook::Sources::KindleDevice::Entry.new do |e|
47
+ e.title = 'Book three'
48
+ e.author = 'Author two'
49
+ e.type = :highlight
50
+ e.added_on = DateTime.new(2012, 10, 11)
51
+ end
52
+ ]
53
+ end
54
+
55
+ it 'returns three books' do
56
+ subject.should have(3).items
57
+ end
58
+
59
+ it 'returns books sorted by last_update descending' do
60
+ subject.map(&:title).should == [ 'Book two', 'Book three', 'Book one' ]
61
+ end
62
+
63
+ context 'and max_books set to 2' do
64
+
65
+ let (:max_books) { 2 }
66
+
67
+ it 'returns two books' do
68
+ subject.should have(2).items
69
+ end
70
+ end
71
+ end
72
+
73
+ context 'with entries for a single book' do
74
+ let(:entries) do
75
+ [
76
+ Klipbook::Sources::KindleDevice::Entry.new do |e|
77
+ e.title = 'Book one'
78
+ e.author = 'Author one'
79
+ e.type = :bookmark
80
+ e.location = 1
81
+ e.page = 1
82
+ e.added_on = DateTime.new(2012, 10, 10)
83
+ e.text = 'First one'
84
+ end,
85
+ Klipbook::Sources::KindleDevice::Entry.new do |e|
86
+ e.title = 'Book one'
87
+ e.author = 'Author one'
88
+ e.type = :highlight
89
+ e.location = 10
90
+ e.page = 3
91
+ e.added_on = DateTime.new(2012, 10, 11)
92
+ e.text = 'Second one'
93
+ end,
94
+ Klipbook::Sources::KindleDevice::Entry.new do |e|
95
+ e.title = 'Book one'
96
+ e.author = 'Author one'
97
+ e.type = :highlight
98
+ e.location = 3
99
+ e.page = 2
100
+ e.added_on = DateTime.new(2012, 10, 1)
101
+ e.text = 'Third one'
102
+ end,
103
+ Klipbook::Sources::KindleDevice::Entry.new do |e|
104
+ e.title = 'Book one'
105
+ e.author = 'Author one'
106
+ e.type = :note
107
+ e.location = 2
108
+ e.page = 1
109
+ e.added_on = DateTime.new(2012, 10, 21)
110
+ e.text = 'Fourth one'
111
+ end
112
+ ]
113
+ end
114
+
115
+ it 'returns a single book with the correct title and author information' do
116
+ subject.first.title.should == 'Book one'
117
+ subject.first.author.should == 'Author one'
118
+ end
119
+
120
+ it "returns a single book with last update equal to the latest added on date of the book's entries" do
121
+ subject.first.last_update.should == DateTime.new(2012, 10, 21)
122
+ end
123
+
124
+ it "ignores bookmarks when building the book's clipping list" do
125
+ subject.first.clippings.should have(3).items
126
+ subject.first.clippings.map(&:type).should_not include(:bookmark)
127
+ end
128
+
129
+ it 'returns a single book whose clippings are sorted by location' do
130
+ subject.first.clippings.map(&:location).should == [2, 3, 10]
131
+ subject.first.clippings.map(&:text).should == ['Fourth one', 'Third one', 'Second one']
132
+ subject.first.clippings.map(&:page).should == [1, 2, 3]
133
+ end
134
+ end
135
+
136
+ context 'with entries for a single book that are all bookmarks' do
137
+ let(:entries) do
138
+ [
139
+ Klipbook::Sources::KindleDevice::Entry.new do |e|
140
+ e.title = 'Book one'
141
+ e.author = 'Author one'
142
+ e.type = :bookmark
143
+ e.location = 1
144
+ e.page = 1
145
+ e.added_on = DateTime.new(2012, 10, 10)
146
+ e.text = 'First one'
147
+ end,
148
+ Klipbook::Sources::KindleDevice::Entry.new do |e|
149
+ e.title = 'Book one'
150
+ e.author = 'Author one'
151
+ e.type = :bookmark
152
+ e.location = 10
153
+ e.page = 3
154
+ e.added_on = DateTime.new(2012, 10, 11)
155
+ e.text = 'Second one'
156
+ end
157
+ ]
158
+ end
159
+
160
+ it { should be_empty }
161
+ end
162
+ end
163
+ end