klipbook 0.1.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.
@@ -0,0 +1,82 @@
1
+ module Klipbook
2
+ class ClippingsParser
3
+ def extract_clippings_from(file_text)
4
+ clippings_text_from(file_text).map { |clipping_text| build_clipping_from(clipping_text) }.compact
5
+ end
6
+
7
+ def build_clipping_from(clipping_text)
8
+ return nil if clipping_text.blank?
9
+
10
+ attributes = extract_attributes(clipping_text)
11
+
12
+ Clipping.new(attributes)
13
+ end
14
+
15
+ def extract_attributes(clipping_text)
16
+
17
+ lines = clipping_text.lstrip.lines.to_a
18
+
19
+ return nil if lines.length < 2
20
+
21
+ title_line = lines[0].strip
22
+ metadata = lines[1].strip
23
+ text_lines = lines[3..-1]
24
+
25
+ return nil unless valid_metadata?(metadata)
26
+
27
+ {
28
+ title: extract_title(title_line),
29
+ author: extract_author(title_line),
30
+ type: extract_type(metadata),
31
+ location: extract_location(metadata),
32
+ added_on: extract_added_date(metadata),
33
+ text: extract_text(text_lines)
34
+ }
35
+ end
36
+
37
+ private
38
+
39
+ def clippings_text_from(file_text)
40
+ file_text.gsub("\r", '').split('==========')
41
+ end
42
+
43
+ def valid_metadata?(metadata)
44
+ metadata.match(/^-.*Added on/)
45
+ end
46
+
47
+ def extract_title(title_line)
48
+ if title_line =~ /\(.+\)\Z/
49
+ title_line.scan(/(.*)\s+\(.+\)\Z/).first.first
50
+ else
51
+ title_line
52
+ end
53
+ end
54
+
55
+ def extract_author(title_line)
56
+ match = title_line.scan /\(([^\(]+)\)\Z/
57
+ match.empty? ? nil : match.first.first
58
+ end
59
+
60
+ def extract_type(metadata)
61
+ type = metadata.scan(/^- (\w+)/).first.first
62
+ type.downcase.to_sym
63
+ end
64
+
65
+ def extract_added_date(metadata)
66
+ metadata.scan(/Added on (.+)$/i).first.first
67
+ end
68
+
69
+ def extract_location(metadata)
70
+ match = metadata.scan(/Loc\. ([0-9]+-?)/)
71
+
72
+ return nil if match.empty?
73
+
74
+ location = match.first.first
75
+ location.to_i
76
+ end
77
+
78
+ def extract_text(text_lines)
79
+ text_lines.join('').rstrip
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,29 @@
1
+ module Klipbook
2
+ class Runner
3
+ def initialize(input_file)
4
+ @clippings_file = Klipbook::ClippingsFile.new(input_file.read.strip)
5
+ end
6
+
7
+ def list_books(output=$stdout)
8
+ if @clippings_file.books.empty?
9
+ output.puts 'Your clippings file contains no books'
10
+ else
11
+ output.puts 'The list of books in your clippings file:'
12
+ @clippings_file.books.each_with_index do |book, index|
13
+ author = book.author ? " by #{book.author}" : ''
14
+ output.puts "[#{index + 1}] #{book.title}#{author}"
15
+ end
16
+ end
17
+ end
18
+
19
+ def print_book_summary(book_number, output)
20
+ if book_number < 1 or book_number > @clippings_file.books.length
21
+ $stderr.puts "Sorry but you must specify a book index between 1 and #{@clippings_file.books.length}"
22
+ return
23
+ end
24
+
25
+ book_summary = @clippings_file.books[book_number - 1]
26
+ output.write book_summary.as_html
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,3 @@
1
+ module Klipbook
2
+ VERSION = '0.1.0'
3
+ end
data/lib/klipbook.rb ADDED
@@ -0,0 +1,10 @@
1
+ # encoding: utf-8
2
+ require 'klipbook/blank'
3
+ require 'klipbook/version'
4
+ require 'klipbook/clippings_file'
5
+ require 'klipbook/clipping'
6
+ require 'klipbook/clippings_parser'
7
+ require 'klipbook/book_summary'
8
+ require 'klipbook/runner'
9
+ require 'klipbook/cli'
10
+
@@ -0,0 +1,30 @@
1
+ require 'spec_helper'
2
+
3
+ describe Klipbook::BookSummary do
4
+ describe '#clippings' do
5
+ subject { Klipbook::BookSummary.new('fake title', 'fake author', clippings).clippings }
6
+
7
+ let(:first_clipping) { FakeClipping.new(order: 10) }
8
+ let(:second_clipping) { FakeClipping.new(order: 22) }
9
+ let(:third_clipping) { FakeClipping.new(order: 200) }
10
+
11
+ let(:clippings) do
12
+ [
13
+ third_clipping,
14
+ second_clipping,
15
+ first_clipping
16
+ ]
17
+ end
18
+
19
+ it 'provides clippings in sorted order' do
20
+ subject.should == [ first_clipping, second_clipping, third_clipping ]
21
+ end
22
+ end
23
+
24
+ # A fake clipping that will naturally sort even if the order attribute isn't set
25
+ class FakeClipping < OpenStruct
26
+ def <=>(other)
27
+ self.order <=> other.order
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,17 @@
1
+ require 'spec_helper'
2
+
3
+ describe Klipbook::Clipping do
4
+ it 'treats the added_on attribute as a date when created' do
5
+ clipping = Klipbook::Clipping.new(added_on: "Sunday, December 4, 2011, 07:33 AM")
6
+ clipping.added_on.should == DateTime.new(2011,12,4,7,33)
7
+ end
8
+
9
+ it 'sort based upon location, treating nils as 0' do
10
+ first_clipping = Klipbook::Clipping.new(location: nil)
11
+ second_clipping = Klipbook::Clipping.new(location: 23)
12
+ third_clipping = Klipbook::Clipping.new(location: 345)
13
+
14
+ sorted_clippings = [second_clipping, third_clipping, first_clipping].sort
15
+ sorted_clippings.should == [ first_clipping, second_clipping, third_clipping ]
16
+ end
17
+ end
@@ -0,0 +1,60 @@
1
+ require 'spec_helper'
2
+ require 'ostruct'
3
+
4
+ describe Klipbook::ClippingsFile do
5
+
6
+ let(:clippings_file) { Klipbook::ClippingsFile.new('', parser) }
7
+
8
+ let (:parser) do
9
+ fake_parser = Object.new
10
+ stub(fake_parser).extract_clippings_from { clippings }
11
+ fake_parser
12
+ end
13
+
14
+ describe '#books' do
15
+ subject { clippings_file.books }
16
+
17
+ context 'with an empty clippings file' do
18
+ let(:clippings) { [] }
19
+
20
+ it { should be_empty }
21
+ end
22
+
23
+ context 'with a single clipping for a single book' do
24
+ let(:clippings) do
25
+ [ OpenStruct.new(title: 'First fake book title') ]
26
+ end
27
+
28
+ it 'contains an entry for the book' do
29
+ subject.map(&:title).should == [ 'First fake book title' ]
30
+ end
31
+ end
32
+
33
+ context 'with clippings for two books' do
34
+ let(:clippings) do
35
+ [
36
+ OpenStruct.new(title: 'Second fake book title'),
37
+ OpenStruct.new(title: 'First fake book title')
38
+ ]
39
+ end
40
+
41
+ it 'contains entries for the two books in alphabetical order' do
42
+ subject.map(&:title).should == [ 'First fake book title', 'Second fake book title' ]
43
+ end
44
+ end
45
+
46
+ context 'with multiple clippings for two books' do
47
+ let(:clippings) do
48
+ [
49
+ OpenStruct.new(title: 'First fake book title'),
50
+ OpenStruct.new(title: 'Second fake book title'),
51
+ OpenStruct.new(title: 'First fake book title')
52
+ ]
53
+ end
54
+
55
+ it 'contains entries for the two books in alphabetical order' do
56
+ subject.map(&:title).should == [ 'First fake book title', 'Second fake book title' ]
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,212 @@
1
+ require 'spec_helper'
2
+
3
+ describe Klipbook::ClippingsParser do
4
+
5
+ describe '#extract_attributes' do
6
+
7
+ subject { Klipbook::ClippingsParser.new.extract_attributes(content) }
8
+
9
+ context 'on a clipping with the title line "Book title (Author\'s Name)"' do
10
+ let (:content) do
11
+ "Book title (Author's Name)\n" +
12
+ "- Highlight Loc. 466-69 | Added on Thursday, April 21, 2011, 07:31 AM\n" +
13
+ "\n" +
14
+ "Highlight text\n"
15
+ end
16
+
17
+ it 'extracts the title as "Book title"' do
18
+ subject[:title].should == 'Book title'
19
+ end
20
+
21
+ it 'extracts the author as "Author\'s Name"' do
22
+ subject[:author].should == "Author's Name"
23
+ end
24
+ end
25
+
26
+ context 'on a clipping with the title line "Book title (sub title) (Author\'s Name)"' do
27
+ let (:content) do
28
+ "Book title (sub title) (Author's Name)\n" +
29
+ "- Highlight Loc. 466-69 | Added on Thursday, April 21, 2011, 07:31 AM\n" +
30
+ "\n" +
31
+ "Highlight text\n"
32
+ end
33
+
34
+ it 'extracts the title as "Book title (sub title)"' do
35
+ subject[:title].should == 'Book title (sub title)'
36
+ end
37
+
38
+ it 'extracts the author "Author\'s Name"' do
39
+ subject[:author].should == "Author's Name"
40
+ end
41
+ end
42
+
43
+ context 'on a clipping with the title line "Book title"' do
44
+ let (:content) do
45
+ "Book title\n" +
46
+ "- Highlight Loc. 466-69 | Added on Thursday, April 21, 2011, 07:31 AM\n" +
47
+ "\n" +
48
+ "Highlight text\n"
49
+ end
50
+
51
+ it 'extracts the title as "Book title"' do
52
+ subject[:title].should == 'Book title'
53
+ end
54
+
55
+ it 'extracts no author' do
56
+ subject[:author].should be_nil
57
+ end
58
+ end
59
+
60
+ context 'on a highlight' do
61
+ let (:content) do
62
+ "Book title\n" +
63
+ "- Highlight Loc. 466-69 | Added on Thursday, April 21, 2011, 07:31 AM\n" +
64
+ "\n" +
65
+ "The first line of the highlight\n" +
66
+ "The second line of the highlight"
67
+ end
68
+
69
+ it 'marks the clipping as a highlight' do
70
+ subject[:type].should == :highlight
71
+ end
72
+
73
+ it 'extracts the highlighted text' do
74
+ subject[:text].should == "The first line of the highlight\nThe second line of the highlight"
75
+ end
76
+ end
77
+
78
+ context 'on a note' do
79
+ let (:content) do
80
+ "Book title\n" +
81
+ "- Note Loc. 623 | Added on Thursday, April 21, 2011, 07:31 AM\n" +
82
+ "\n" +
83
+ "The note text"
84
+ end
85
+
86
+ it 'marks the clipping as a note' do
87
+ subject[:type].should == :note
88
+ end
89
+
90
+ it 'extracts the note text' do
91
+ subject[:text].should == "The note text"
92
+ end
93
+ end
94
+
95
+ context 'on a bookmark' do
96
+ let (:content) do
97
+ "Book title\n" +
98
+ "- Bookmark on Page 1 | Loc. 406 | Added on Thursday, April 21, 2011, 07:31 AM\n" +
99
+ "\n" +
100
+ "\n"
101
+ end
102
+
103
+ it 'marks the clipping as a bookmark' do
104
+ subject[:type].should == :bookmark
105
+ end
106
+
107
+ it 'extracts empty text' do
108
+ subject[:text].should == ''
109
+ end
110
+ end
111
+
112
+ context 'on a clipping with a single location value' do
113
+ let (:content) do
114
+ "Book title\n" +
115
+ "- Highlight Loc. 465 | Added on Thursday, April 21, 2011, 07:31 AM\n" +
116
+ "\n" +
117
+ "The first line of the highlight\n" +
118
+ "The second line of the highlight"
119
+ end
120
+
121
+ it 'extracts the location' do
122
+ subject[:location].should == 465
123
+ end
124
+
125
+ it 'extracts the added_on date' do
126
+ subject[:added_on].should == 'Thursday, April 21, 2011, 07:31 AM'
127
+ end
128
+ end
129
+
130
+ context 'on a clipping with a location range' do
131
+ let (:content) do
132
+ "Book title\n" +
133
+ "- Highlight Loc. 466-69 | Added on Thursday, April 21, 2011, 07:31 AM\n" +
134
+ "\n" +
135
+ "The first line of the highlight\n" +
136
+ "The second line of the highlight"
137
+ end
138
+
139
+ it 'extracts the first element of the location range' do
140
+ subject[:location].should == 466
141
+ end
142
+
143
+ it 'extracts the added_on date' do
144
+ subject[:added_on].should == 'Thursday, April 21, 2011, 07:31 AM'
145
+ end
146
+ end
147
+
148
+ context 'on a highlight with a page number and location range' do
149
+ let (:content) do
150
+ "Book title\n" +
151
+ "- Highlight on Page 171 | Loc. 1858-59 | Added on Thursday, April 21, 2011, 07:31 AM\n" +
152
+ "\n" +
153
+ "Some highlighted text\n" +
154
+ "\n"
155
+ end
156
+
157
+ it 'extracts the first element of the location range' do
158
+ subject[:location].should == 1858
159
+ end
160
+
161
+ it 'extracts the date' do
162
+ subject[:added_on].should == 'Thursday, April 21, 2011, 07:31 AM'
163
+ end
164
+ end
165
+
166
+ context 'on a highlight with a page number and no location' do
167
+ let (:content) do
168
+ "Book title\n" +
169
+ "- Highlight on Page 9 | Added on Thursday, April 21, 2011, 07:31 AM\n" +
170
+ "\n" +
171
+ "Clipping"
172
+ end
173
+
174
+ it 'extracts no location' do
175
+ subject[:location].should be_nil
176
+ end
177
+
178
+ it 'extracts the date' do
179
+ subject[:added_on].should == 'Thursday, April 21, 2011, 07:31 AM'
180
+ end
181
+ end
182
+ end
183
+
184
+ describe '#build_clipping_from' do
185
+
186
+ let(:it) { Klipbook::ClippingsParser.new }
187
+
188
+ context 'with an empty string' do
189
+ subject { it.build_clipping_from(" ") }
190
+
191
+ it { should be_nil }
192
+ end
193
+
194
+ it 'builds a clipping using the extracted attributes' do
195
+ attributes = {
196
+ title: 'Dummy title',
197
+ type: :highlight,
198
+ location: 42,
199
+ added_on: '10 August 2011',
200
+ text: 'Some highlighted text'
201
+ }
202
+ stub(it).extract_attributes { attributes }
203
+ stub(Klipbook::Clipping).new
204
+
205
+ it.build_clipping_from('Dummy content')
206
+
207
+ Klipbook::Clipping.should have_received.new.with(attributes)
208
+ end
209
+ end
210
+ end
211
+
212
+
@@ -0,0 +1,87 @@
1
+ require 'spec_helper'
2
+
3
+ describe Klipbook::Runner do
4
+
5
+ let (:input_file) do
6
+ input_file = Object.new
7
+ stub(input_file).read { '' }
8
+ input_file
9
+ end
10
+
11
+ let (:output) do
12
+ output = Object.new
13
+ stub(output).puts
14
+ output
15
+ end
16
+
17
+ let (:clippings_file) do
18
+ clipplings = Object.new
19
+ stub(clipplings).books { books }
20
+ clipplings
21
+ end
22
+
23
+ before :each do
24
+ stub(Klipbook::ClippingsFile).new { clippings_file }
25
+ end
26
+
27
+ describe '#list_books' do
28
+
29
+ subject { Klipbook::Runner.new(input_file).list_books(output) }
30
+
31
+ context 'with an empty clippings file' do
32
+
33
+ let (:books) { [] }
34
+
35
+ it 'displays a message saying that the clipping file contains no books' do
36
+ subject
37
+ output.should have_received.puts('Your clippings file contains no books')
38
+ end
39
+ end
40
+
41
+ context 'with a clipping file with multiple books' do
42
+ let (:books) do
43
+ [ OpenStruct.new(title: 'My first fake title'), OpenStruct.new(title: 'Another fake book', author: 'Rock Riphard') ]
44
+ end
45
+
46
+ it 'displays a message describing the book list' do
47
+ subject
48
+ output.should have_received.puts('The list of books in your clippings file:')
49
+ end
50
+
51
+ it 'displays an indexed list of book titles including the author when available' do
52
+ subject
53
+ output.should have_received.puts('[1] My first fake title')
54
+ output.should have_received.puts('[2] Another fake book by Rock Riphard')
55
+ end
56
+ end
57
+ end
58
+
59
+ describe '#print_book_summary' do
60
+
61
+ describe 'with a clipping files with two books' do
62
+ let (:book_two) do
63
+ book_two = Object.new
64
+ end
65
+
66
+ let (:books) do
67
+ [ OpenStruct.new(title: 'My first fake title'), book_two ]
68
+ end
69
+
70
+ [0, 3].each do |book_number|
71
+ it "displays an error message if the book index requested is #{book_number}" do
72
+ stub($stderr).puts
73
+ Klipbook::Runner.new(input_file).print_book_summary(book_number, output)
74
+ $stderr.should have_received.puts('Sorry but you must specify a book index between 1 and 2')
75
+ end
76
+ end
77
+
78
+ it 'outputs the html summary of the book selected' do
79
+ mock_html_output = Object.new
80
+ stub(output).write
81
+ stub(book_two).as_html { mock_html_output }
82
+ Klipbook::Runner.new(input_file).print_book_summary(2, output)
83
+ output.should have_received.write(mock_html_output)
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,14 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+
4
+ require 'rspec'
5
+ require 'rr'
6
+
7
+ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
8
+
9
+ $LOAD_PATH << File.expand_path('../../../lib', __FILE__)
10
+ require 'klipbook'
11
+
12
+ RSpec.configure do |config|
13
+ config.mock_with :rr
14
+ end
@@ -0,0 +1,25 @@
1
+ module RR
2
+ module Adapters
3
+ module RSpec2
4
+
5
+ include RRMethods
6
+
7
+ def setup_mocks_for_rspec
8
+ RR.reset
9
+ end
10
+
11
+ def verify_mocks_for_rspec
12
+ RR.verify
13
+ end
14
+
15
+ def teardown_mocks_for_rspec
16
+ RR.reset
17
+ end
18
+
19
+ def have_received(method = nil)
20
+ RR::Adapters::Rspec::InvocationMatcher.new(method)
21
+ end
22
+ end
23
+ end
24
+ end
25
+
@@ -0,0 +1,12 @@
1
+ require 'rr'
2
+
3
+ RSpec.configuration.backtrace_clean_patterns.push(RR::Errors::BACKTRACE_IDENTIFIER)
4
+
5
+ module RSpec
6
+ module Core
7
+ module MockFrameworkAdapter
8
+ include RR::Adapters::RSpec2
9
+ end
10
+ end
11
+ end
12
+