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.
- data/Gemfile +6 -5
- data/Gemfile.lock +62 -25
- data/Guardfile +1 -2
- data/README.md +66 -18
- data/Rakefile +5 -1
- data/bin/klipbook +85 -1
- data/example.png +0 -0
- data/features/collate.feature +51 -0
- data/features/fixtures/clippings-for-three-books.txt +105 -0
- data/features/list.feature +31 -0
- data/features/step_definitions/collate_steps.rb +61 -0
- data/features/step_definitions/list_steps.rb +15 -0
- data/features/support/env.rb +5 -1
- data/klipbook.gemspec +49 -32
- data/lib/klipbook/book.rb +18 -0
- data/lib/klipbook/clipping.rb +4 -10
- data/lib/klipbook/collator.rb +17 -0
- data/lib/klipbook/config.rb +22 -0
- data/lib/klipbook/fetcher.rb +29 -0
- data/lib/klipbook/invalid_source_error.rb +12 -0
- data/lib/klipbook/output/book_helpers.rb +12 -0
- data/lib/klipbook/{book_summary.erb → output/html_book_summary.erb} +65 -11
- data/lib/klipbook/output/html_summary_writer.rb +42 -0
- data/lib/klipbook/printer.rb +18 -0
- data/lib/klipbook/sources/amazon_site/book_scraper.rb +67 -0
- data/lib/klipbook/sources/amazon_site/scraper.rb +78 -0
- data/lib/klipbook/sources/kindle_device/entry.rb +11 -0
- data/lib/klipbook/sources/kindle_device/entry_parser.rb +85 -0
- data/lib/klipbook/sources/kindle_device/file.rb +57 -0
- data/lib/klipbook/sources/kindle_device/file_parser.rb +33 -0
- data/lib/klipbook/version.rb +1 -1
- data/lib/klipbook.rb +18 -5
- data/spec/lib/klipbook/book_spec.rb +33 -0
- data/spec/lib/klipbook/collator_spec.rb +40 -0
- data/spec/lib/klipbook/fetcher_spec.rb +81 -0
- data/spec/lib/klipbook/output/html_summary_writer_spec.rb +90 -0
- data/spec/lib/klipbook/printer_spec.rb +45 -0
- data/spec/lib/klipbook/sources/kindle_device/entry_parser_spec.rb +275 -0
- data/spec/lib/klipbook/sources/kindle_device/file_parser_spec.rb +68 -0
- data/spec/lib/klipbook/sources/kindle_device/file_spec.rb +163 -0
- metadata +158 -58
- data/features/list_books.feature +0 -23
- data/features/print_book_summary.feature +0 -10
- data/features/step_definitions/klipbook_steps.rb +0 -87
- data/lib/klipbook/book_summary.rb +0 -35
- data/lib/klipbook/cli.rb +0 -49
- data/lib/klipbook/clippings_file.rb +0 -50
- data/lib/klipbook/clippings_parser.rb +0 -98
- data/lib/klipbook/runner.rb +0 -29
- data/spec/lib/klipbook/book_summary_spec.rb +0 -30
- data/spec/lib/klipbook/clipping_spec.rb +0 -17
- data/spec/lib/klipbook/clippings_file_spec.rb +0 -60
- data/spec/lib/klipbook/clippings_parser_spec.rb +0 -367
- 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
|
data/lib/klipbook/version.rb
CHANGED
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
|
-
|
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
|