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,85 @@
1
+ module Klipbook::Sources
2
+ module KindleDevice
3
+ class EntryParser
4
+
5
+ def build_entry(entry_text)
6
+ return nil if invalid_entry?(entry_text)
7
+
8
+ lines = split_text_into_lines(entry_text)
9
+ title_line = lines[0].strip
10
+ metadata = lines[1].strip
11
+ text_lines = lines[3..-1]
12
+
13
+ type = extract_type(metadata)
14
+
15
+ Klipbook::Sources::KindleDevice::Entry.new do |h|
16
+ h.title = extract_title(title_line)
17
+ h.author = extract_author(title_line)
18
+ h.location = extract_location(metadata)
19
+ h.page = extract_page(metadata)
20
+ h.added_on = extract_added_date(metadata)
21
+ h.text = extract_content(text_lines)
22
+ h.type = extract_type(metadata)
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ def invalid_entry?(entry_text)
29
+ entry_text.blank? || incomplete_entry?(entry_text)
30
+ end
31
+
32
+ def incomplete_entry?(entry_text)
33
+ split_text_into_lines(entry_text).length < 2
34
+ end
35
+
36
+ def split_text_into_lines(entry_text)
37
+ entry_text.lstrip.lines.to_a
38
+ end
39
+
40
+ def extract_title(title_line)
41
+ if title_line =~ /\(.+\)\Z/
42
+ title_line.scan(/(.*)\s+\(.+\)\Z/).first.first
43
+ else
44
+ title_line
45
+ end
46
+ end
47
+
48
+ def extract_author(title_line)
49
+ match = title_line.scan /\(([^\(]+)\)\Z/
50
+ match.empty? ? nil : match.first.first
51
+ end
52
+
53
+ def extract_type(metadata)
54
+ type = metadata.scan(/^-( Your)? (\w+)/).first[1]
55
+ type.downcase.to_sym
56
+ end
57
+
58
+ def extract_location(metadata)
59
+ match = metadata.scan(/Loc(ation|\.) ([0-9]+-?)/)
60
+
61
+ return 0 if match.empty?
62
+
63
+ location = match.first[1]
64
+ location.to_i
65
+ end
66
+
67
+ def extract_page(metadata)
68
+ match = metadata.scan(/Page (\d+)/)
69
+
70
+ return nil if match.empty?
71
+
72
+ location = match.first.first
73
+ location.to_i
74
+ end
75
+
76
+ def extract_content(text_lines)
77
+ text_lines.join('').rstrip
78
+ end
79
+
80
+ def extract_added_date(metadata)
81
+ DateTime.parse(metadata.scan(/Added on (.+)$/i).first.first)
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,57 @@
1
+ module Klipbook::Sources
2
+ module KindleDevice
3
+ class File
4
+ def initialize(file_text, max_books, file_parser=FileParser.new)
5
+ @file_text = file_text
6
+ @file_parser = file_parser
7
+ @max_books = max_books
8
+ end
9
+
10
+ def books
11
+ @books ||= build_books.take(@max_books)
12
+ end
13
+
14
+ private
15
+
16
+ def build_books
17
+ sorted_entries = extract_sorted_entries_from_file_text
18
+ build_sorted_book_list(sorted_entries)
19
+ end
20
+
21
+ def extract_sorted_entries_from_file_text
22
+ entries = @file_parser.extract_entries(@file_text)
23
+ entries.sort { |entry_a, entry_b| entry_a.title <=> entry_b.title }
24
+ end
25
+
26
+ def build_sorted_book_list(sorted_entries)
27
+ books_from_entries(sorted_entries).sort do |book_a, book_b|
28
+ -(book_a.last_update <=> book_b.last_update)
29
+ end
30
+ end
31
+
32
+ def books_from_entries(entries)
33
+ entries.select { |entry| entry.type != :bookmark }
34
+ .group_by(&:title)
35
+ .map { |title, book_entries| book_from_entries(book_entries) }
36
+ end
37
+
38
+ def book_from_entries(entries)
39
+ entries.sort! { |ea, eb| ea.location <=> eb.location }
40
+
41
+ Klipbook::Book.new do |b|
42
+ b.title = entries.first.title
43
+ b.author = entries.first.author
44
+ b.last_update = entries.map(&:added_on).max
45
+ b.clippings = entries.map do |e|
46
+ Klipbook::Clipping.new do |c|
47
+ c.location = e.location
48
+ c.page = e.page
49
+ c.text = e.text
50
+ c.type = e.type
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,33 @@
1
+ # encoding: UTF-8
2
+
3
+ module Klipbook::Sources
4
+ module KindleDevice
5
+ class FileParser
6
+ def initialize(entry_parser=EntryParser.new)
7
+ @entry_parser = entry_parser
8
+ end
9
+
10
+ def extract_entries(file_text)
11
+ entries_text = split_into_raw_entries_text(file_text)
12
+
13
+ build_entries(entries_text)
14
+ end
15
+
16
+ private
17
+
18
+ def build_entries(entries_text)
19
+ entries_text.map do |entry_text|
20
+ @entry_parser.build_entry(entry_text)
21
+ end.compact
22
+ end
23
+
24
+ def strip_control_characters(file_text)
25
+ file_text.gsub("\r", '').gsub("\xef\xbb\xbf", '')
26
+ end
27
+
28
+ def split_into_raw_entries_text(file_text)
29
+ strip_control_characters(file_text).split('==========')
30
+ end
31
+ end
32
+ end
33
+ end
@@ -1,3 +1,3 @@
1
1
  module Klipbook
2
- VERSION = '0.3.0'
2
+ VERSION = '1.0.0'
3
3
  end
data/lib/klipbook.rb CHANGED
@@ -1,10 +1,23 @@
1
1
  # encoding: utf-8
2
2
  require 'klipbook/blank'
3
3
  require 'klipbook/version'
4
- require 'klipbook/clippings_file'
4
+
5
+ require 'klipbook/sources/kindle_device/file_parser'
6
+ require 'klipbook/sources/kindle_device/entry_parser'
7
+ require 'klipbook/sources/kindle_device/entry'
8
+ require 'klipbook/sources/kindle_device/file'
9
+
10
+ require 'klipbook/sources/amazon_site/scraper'
11
+ require 'klipbook/sources/amazon_site/book_scraper'
12
+
13
+ require 'klipbook/invalid_source_error'
14
+ require 'klipbook/config'
15
+ require 'klipbook/book'
5
16
  require 'klipbook/clipping'
6
- require 'klipbook/clippings_parser'
7
- require 'klipbook/book_summary'
8
- require 'klipbook/runner'
9
- require 'klipbook/cli'
10
17
 
18
+ require 'klipbook/fetcher'
19
+ require 'klipbook/collator'
20
+ require 'klipbook/printer'
21
+
22
+ require 'klipbook/output/html_summary_writer'
23
+ require 'klipbook/output/book_helpers'
@@ -0,0 +1,33 @@
1
+ require 'spec_helper'
2
+
3
+ describe Klipbook::Book do
4
+ describe '#title_and_author' do
5
+
6
+ subject { book.title_and_author }
7
+
8
+ context 'with no author' do
9
+ let(:book) do
10
+ Klipbook::Book.new do |b|
11
+ b.title = 'Book title'
12
+ end
13
+ end
14
+
15
+ it 'only contains the title' do
16
+ subject.should == 'Book title'
17
+ end
18
+ end
19
+
20
+ context 'with an author' do
21
+ let(:book) do
22
+ Klipbook::Book.new do |b|
23
+ b.title = 'Book title'
24
+ b.author = 'Rob Ripjaw'
25
+ end
26
+ end
27
+
28
+ it 'contains the title and author' do
29
+ subject.should == 'Book title by Rob Ripjaw'
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,40 @@
1
+ require 'spec_helper'
2
+
3
+ describe Klipbook::Collator do
4
+
5
+ let (:it) { Klipbook::Collator.new(books, summary_writer) }
6
+
7
+ let (:summary_writer) do
8
+ Object.new.tap do |fake_writer|
9
+ stub(fake_writer).write
10
+ end
11
+ end
12
+
13
+ let (:message_stream) do
14
+ Object.new.tap do |fake_stream|
15
+ stub(fake_stream).puts
16
+ end
17
+ end
18
+
19
+ let (:book_one) { Klipbook::Book.new }
20
+ let (:book_two) { Klipbook::Book.new }
21
+ let (:books) { [ book_one, book_two ] }
22
+
23
+ let(:output_dir) { 'fake output dir' }
24
+
25
+ describe '#collate_books' do
26
+
27
+ subject { it.collate_books(output_dir, true, message_stream) }
28
+
29
+ it 'prints a message displaying the output directory' do
30
+ subject
31
+ message_stream.should have_received.puts('Using output directory: fake output dir')
32
+ end
33
+
34
+ it 'passes each book to the summary writer' do
35
+ subject
36
+ summary_writer.should have_received.write(book_one, output_dir, true)
37
+ summary_writer.should have_received.write(book_two, output_dir, true)
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,81 @@
1
+ require 'spec_helper'
2
+
3
+ describe Klipbook::Fetcher do
4
+
5
+ let(:fake_source) do
6
+ Object.new.tap do |fakey|
7
+ stub(fakey).books { books }
8
+ end
9
+ end
10
+
11
+ context 'when created with an invalid source' do
12
+ it 'raises an error' do
13
+ expect { Klipbook::Fetcher.new('sdf' , 2) }.to raise_error(InvalidSourceError)
14
+ end
15
+ end
16
+
17
+ context 'when created with a site source' do
18
+ subject { fetcher }
19
+
20
+ let(:fetcher) { Klipbook::Fetcher.new('site:username@example.com:password', 2) }
21
+
22
+ before(:each) do
23
+ stub(Klipbook::Sources::AmazonSite::Scraper).new { fake_source }
24
+ end
25
+
26
+ it 'creates a site scraper with the provided credentials' do
27
+ subject
28
+ Klipbook::Sources::AmazonSite::Scraper.should have_received.new('username@example.com', 'password', 2)
29
+ end
30
+
31
+ describe '#fetch_books' do
32
+ subject { fetcher.fetch_books }
33
+
34
+ let(:books) { [] }
35
+
36
+ it 'returns the books returned by the site source' do
37
+ subject.should == books
38
+ end
39
+ end
40
+ end
41
+
42
+ context 'when created with a file source' do
43
+ subject { fetcher }
44
+
45
+ let(:fetcher) { Klipbook::Fetcher.new('file:filename', 2) }
46
+
47
+ let(:fake_file) do
48
+ Object.new.tap do |file|
49
+ stub(file).read { file_contents }
50
+ end
51
+ end
52
+
53
+ let(:file_contents) { 'fake contents ' }
54
+
55
+ before(:each) do
56
+ stub(Klipbook::Sources::KindleDevice::File).new { fake_source }
57
+ stub(File).open { fake_file }
58
+ end
59
+
60
+ it "reads the file's contents from disk" do
61
+ subject
62
+ File.should have_received.open('filename', 'r')
63
+ end
64
+
65
+ it 'uses the file contents to create a file source' do
66
+ subject
67
+
68
+ Klipbook::Sources::KindleDevice::File.should have_received.new('fake contents', 2)
69
+ end
70
+
71
+ describe '#fetch_books' do
72
+ subject { fetcher.fetch_books }
73
+
74
+ let(:books) { [] }
75
+
76
+ it 'returns the books returned by the file source' do
77
+ subject.should == books
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,90 @@
1
+ require 'spec_helper'
2
+
3
+ # This is more of an integration test but what the heck
4
+ # it can live in here for now
5
+
6
+ describe Klipbook::Output::HtmlSummaryWriter do
7
+
8
+ before(:all) do
9
+ @output_dir = Dir.mktmpdir
10
+ end
11
+
12
+ after(:all) do
13
+ FileUtils.rm_f(@output_dir)
14
+ end
15
+
16
+ let(:book) do
17
+ Klipbook::Book.new do |b|
18
+ b.title = 'Fake book title'
19
+ b.author = 'Fake Author'
20
+ b.clippings = []
21
+ end
22
+ end
23
+
24
+ let(:message_stream) do
25
+ Object.new.tap do |fake_stream|
26
+ stub(fake_stream).puts
27
+ end
28
+ end
29
+
30
+ describe '#write' do
31
+
32
+ subject { Klipbook::Output::HtmlSummaryWriter.new(message_stream).write(book, @output_dir, force) }
33
+
34
+ let(:force) { false }
35
+
36
+ let(:expected_filename) { "Fake book title by Fake Author.html" }
37
+ let(:expected_filepath) { "#{@output_dir}/Fake book title by Fake Author.html" }
38
+
39
+ context 'with no existing summary file' do
40
+ before(:each) do
41
+ FileUtils.rm_f(File.join(@output_dir, '*'))
42
+ end
43
+
44
+ it 'writes a file named after the book into the output directory' do
45
+ subject
46
+ File.exists?(expected_filepath).should be_true
47
+ end
48
+
49
+ it 'writes a html summary to the file' do
50
+ subject
51
+ File.read(expected_filepath).should include("<h1>Fake book title</h1>")
52
+ end
53
+ end
54
+
55
+ context 'with an existing summary file' do
56
+ before(:each) do
57
+ FileUtils.rm_f(expected_filepath)
58
+ FileUtils.touch(expected_filepath)
59
+ end
60
+
61
+ context "and 'force' set to false" do
62
+ let(:force) { false }
63
+
64
+ it "won't write to the file" do
65
+ subject
66
+ File.size(expected_filepath).should == 0
67
+ end
68
+
69
+ it 'prints a message informing that the file is being skipped' do
70
+ subject
71
+ message_stream.should have_received.puts("\e[33mSkipping \e[0m#{expected_filename}")
72
+ end
73
+ end
74
+
75
+ context "and 'force' set to true" do
76
+ let(:force) { true }
77
+
78
+ it 'overwrites the file' do
79
+ subject
80
+ File.size(expected_filepath).should > 0
81
+ end
82
+
83
+ it 'prints a message informing that the file is being written' do
84
+ subject
85
+ message_stream.should have_received.puts("\e[32mWriting \e[0m#{expected_filename}")
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,45 @@
1
+ require 'spec_helper'
2
+
3
+ describe Klipbook::Printer do
4
+
5
+ let (:output) do
6
+ Object.new.tap do |fake_output|
7
+ stub(fake_output).puts
8
+ end
9
+ end
10
+
11
+ describe '#print' do
12
+
13
+ subject { Klipbook::Printer.new(books).print(output) }
14
+
15
+ context 'when created with no books' do
16
+
17
+ let (:books) { [] }
18
+
19
+ it 'displays a message saying that the clipping file contains no books' do
20
+ subject
21
+ output.should have_received.puts('No books available')
22
+ end
23
+ end
24
+
25
+ context 'when created with multiple books' do
26
+ let (:books) do
27
+ [
28
+ Klipbook::Book.new { |b| b.title = 'My first fake title' },
29
+ Klipbook::Book.new { |b| b.title = 'Another fake book'; b.author = 'Rock Riphard' }
30
+ ]
31
+ end
32
+
33
+ it 'displays a message describing the book list' do
34
+ subject
35
+ output.should have_received.puts('Book list:')
36
+ end
37
+
38
+ it 'displays an indexed list of book titles including the author when available' do
39
+ subject
40
+ output.should have_received.puts('[1] My first fake title')
41
+ output.should have_received.puts('[2] Another fake book by Rock Riphard')
42
+ end
43
+ end
44
+ end
45
+ end