klipbook 0.3.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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