klipbook 0.1.0

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