klipbook 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ lib/**/*.rb
2
+ bin/*
3
+ -
4
+ features/**/*.feature
5
+ LICENSE.txt
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --colour --format documentation
data/.yardopts ADDED
@@ -0,0 +1 @@
1
+ -m markdown
data/Gemfile ADDED
@@ -0,0 +1,19 @@
1
+ source 'http://rubygems.org'
2
+
3
+ gem 'thor'
4
+
5
+ group :development do
6
+ gem 'rspec'
7
+ gem 'rr'
8
+ gem 'bundler'
9
+ gem 'jeweler', '~> 1.6.4'
10
+ gem 'rcov', '>= 0'
11
+ gem 'cucumber'
12
+ gem 'guard'
13
+ gem 'guard-rspec'
14
+ gem 'guard-cucumber'
15
+ gem 'rb-inotify', :require => false
16
+ gem 'rb-fsevent', :require => false
17
+ gem 'rb-fchange', :require => false
18
+ gem 'growl_notify'
19
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,67 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ builder (3.0.0)
5
+ cucumber (1.1.3)
6
+ builder (>= 2.1.2)
7
+ diff-lcs (>= 1.1.2)
8
+ gherkin (~> 2.6.7)
9
+ json (>= 1.4.6)
10
+ term-ansicolor (>= 1.0.6)
11
+ diff-lcs (1.1.3)
12
+ ffi (1.0.11)
13
+ gherkin (2.6.7)
14
+ json (>= 1.4.6)
15
+ git (1.2.5)
16
+ growl_notify (0.0.3)
17
+ rb-appscript
18
+ guard (0.8.8)
19
+ thor (~> 0.14.6)
20
+ guard-cucumber (0.7.4)
21
+ cucumber (>= 0.10)
22
+ guard (>= 0.8.3)
23
+ guard-rspec (0.5.5)
24
+ guard (>= 0.8.4)
25
+ jeweler (1.6.4)
26
+ bundler (~> 1.0)
27
+ git (>= 1.2.5)
28
+ rake
29
+ json (1.6.1)
30
+ rake (0.9.2.2)
31
+ rb-appscript (0.6.1)
32
+ rb-fchange (0.0.5)
33
+ ffi
34
+ rb-fsevent (0.4.3.1)
35
+ rb-inotify (0.8.8)
36
+ ffi (>= 0.5.0)
37
+ rcov (0.9.11)
38
+ rr (1.0.4)
39
+ rspec (2.7.0)
40
+ rspec-core (~> 2.7.0)
41
+ rspec-expectations (~> 2.7.0)
42
+ rspec-mocks (~> 2.7.0)
43
+ rspec-core (2.7.1)
44
+ rspec-expectations (2.7.0)
45
+ diff-lcs (~> 1.1.2)
46
+ rspec-mocks (2.7.0)
47
+ term-ansicolor (1.0.7)
48
+ thor (0.14.6)
49
+
50
+ PLATFORMS
51
+ ruby
52
+
53
+ DEPENDENCIES
54
+ bundler
55
+ cucumber
56
+ growl_notify
57
+ guard
58
+ guard-cucumber
59
+ guard-rspec
60
+ jeweler (~> 1.6.4)
61
+ rb-fchange
62
+ rb-fsevent
63
+ rb-inotify
64
+ rcov
65
+ rr
66
+ rspec
67
+ thor
data/Guardfile ADDED
@@ -0,0 +1,19 @@
1
+
2
+ group 'backend' do
3
+
4
+ guard 'rspec', :cli => '--color --format doc' do
5
+ # Regexp watch patterns are matched with Regexp#match
6
+ watch(%r{^spec/.+_spec\.rb$})
7
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
8
+
9
+ # String watch patterns are matched with simple '=='
10
+ watch('spec/spec_helper.rb') { "spec" }
11
+ end
12
+
13
+ guard 'cucumber' do
14
+ watch(%r{^features/.+\.feature$})
15
+ watch(%r{^features/support/.+$}) { 'features' }
16
+ watch(%r{^features/step_definitions/(.+)_steps\.rb$}) { |m| Dir[File.join("**/#{m[1]}.feature")][0] || 'features' }
17
+ end
18
+ end
19
+
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 Ray Grasso
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,51 @@
1
+ # Klipbook
2
+
3
+ Klipbook creates a html summary of your Kindle book clippings.
4
+
5
+ ## How does it work?
6
+
7
+ Copy your clippings file (called "My Clippings.txt" on a 3rd generation Kindle) from your Kindle device to your local drive via USB.
8
+
9
+ **List the books in your clippings file:**
10
+
11
+ $ klipbook list "My Clippings.txt"
12
+
13
+ The list of books in your clippings file:
14
+ [1] The Big Sleep by Raymond Chandler
15
+ [2] How to jump out of a plane without a parachute and survive by Rip Rockjaw
16
+
17
+ **Print a html summary for the book of your choice:**
18
+
19
+ Choose the index of the book you are interested in and print a html summary with the `summarise` command:
20
+
21
+ $ klipbook summarise "My Clippings.txt" 1 big-sleep-clippings.html
22
+
23
+ Keep this nicely formatted html version of your clippings for your own reference.
24
+
25
+ ## Example of a summary file generated by Klipbook
26
+
27
+ <img src="https://github.com/grassdog/klipbook/raw/master/example.png" alt="Example of a summary file" />
28
+
29
+ ## Installation
30
+
31
+ Klipbook is a Ruby gem. To install simply run:
32
+
33
+ gem install klipbook
34
+
35
+ ## Why not just see your clippings on the Amazon site?
36
+
37
+ Currently [the Amazon highlights site](https://kindle.amazon.com/your_highlights) only shows clippings for books you purchased on Amazon.
38
+
39
+ ## Tested platforms
40
+
41
+ Klipbook has been tested on clippings files from 3rd generation Kindles and run using MRI 1.9.3 on Mac OSX Lion and Ubuntu.
42
+
43
+
44
+ ## Contributing to Klipbook
45
+
46
+ Fork the project on [Github](https://github.com/grassdog/klipbook) and submit a pull request.
47
+
48
+ ## Copyright
49
+
50
+ Copyright (c) 2011 Ray Grasso. See LICENSE.txt for further details.
51
+
data/Rakefile ADDED
@@ -0,0 +1,50 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rubygems'
4
+ require 'bundler/setup'
5
+ require 'rake'
6
+ require './lib/klipbook/version.rb'
7
+
8
+ require 'jeweler'
9
+ Jeweler::Tasks.new do |gem|
10
+ gem.name = 'klipbook'
11
+ gem.homepage = 'https://github.com/grassdog/klipbook'
12
+ gem.license = 'MIT'
13
+ gem.summary = %Q{Klipbook creates a HTML summary of your Kindle book clippings.}
14
+ gem.description = %Q{Process your Kindle clippings file to generate a nicely formatted compilation of your clippings of the books you've read}
15
+ gem.email = 'ray.grasso@gmail.com'
16
+ gem.authors = ['Ray Grasso']
17
+ gem.version = Klipbook::VERSION
18
+ # dependencies defined in Gemfile
19
+ end
20
+ Jeweler::RubygemsDotOrgTasks.new
21
+
22
+ require 'rdoc/task'
23
+ Rake::RDocTask.new do |rdoc|
24
+ version = Klipbook::VERSION
25
+
26
+ rdoc.rdoc_dir = 'rdoc'
27
+ rdoc.title = "klipbook #{version}"
28
+ rdoc.rdoc_files.include('README*')
29
+ rdoc.rdoc_files.include('lib/**/*.rb')
30
+ end
31
+
32
+ require 'rspec/core/rake_task'
33
+ desc 'Generate code coverage'
34
+ RSpec::Core::RakeTask.new(:coverage) do |t|
35
+ t.pattern = './spec/**/*_spec.rb' # don't need this, it's default.
36
+ t.rcov = true
37
+ t.rcov_opts = ['--exclude', 'spec']
38
+ end
39
+
40
+ desc 'Run specs'
41
+ RSpec::Core::RakeTask.new(:spec)
42
+
43
+ require 'cucumber/rake/task'
44
+ Cucumber::Rake::Task.new(:features) do |t|
45
+ t.cucumber_opts = '--format progress'
46
+ end
47
+
48
+ desc 'Default: run specs'
49
+ task :default => :spec
50
+
data/bin/klipbook ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH << File.expand_path('../../lib', __FILE__)
4
+ require 'klipbook'
5
+
6
+ Klipbook::CLI.start
7
+
data/example.png ADDED
Binary file
@@ -0,0 +1,23 @@
1
+ Feature: klipbook lists the books in a clipping file
2
+ As an avid reader and note taker
3
+ I want to be shown an indexed list of books in my clipping file
4
+ So that I can choose which book I want to extract a pretty collated list of notes from
5
+
6
+ Scenario: Empty file
7
+ Given I have a file that contains no clippings
8
+ When I list the books in the file with klipbook
9
+ Then I should see the message "Your clippings file contains no books"
10
+
11
+ Scenario: File with one book
12
+ Given I have a file that contains clippings for the book titled "The life of dogs"
13
+ When I list the books in the file with klipbook
14
+ Then I should see the message "The list of books in your clippings file:"
15
+ And I should see the message "[1] The life of dogs"
16
+
17
+ Scenario: File with two books
18
+ Given I have a file that contains clippings for the book titled "The life of dogs"
19
+ And I have a file that contains clippings for the book titled "A hard day's night"
20
+ When I list the books in the file with klipbook
21
+ Then I should see the message "The list of books in your clippings file:"
22
+ And I should see the message "[1] A hard day's night"
23
+ And I should see the message "[2] The life of dogs"
@@ -0,0 +1,10 @@
1
+ Feature: klipbook outputs a pretty summary of the clippings for a book
2
+ As an avid reader and note taker
3
+ I want to see a pretty summary file of the clippings for a book
4
+ So that I can read a nice summary of my clippings for a book I've read
5
+
6
+ Scenario: File with clippings for a book
7
+ Given I have a file that contains multiple clippings for the book titled "The life of dogs"
8
+ When I print the summary for book number "1" with klipbook
9
+ Then I should see a pretty summary for the book "The life of dogs"
10
+
@@ -0,0 +1,87 @@
1
+ Before do
2
+ FileUtils.mkdir_p(TEST_DIR)
3
+ Dir.chdir(TEST_DIR)
4
+ end
5
+
6
+ After do
7
+ Dir.chdir(TEST_DIR)
8
+ FileUtils.rm_rf(TEST_DIR)
9
+ end
10
+
11
+ class Output
12
+ def messages
13
+ @messages ||= []
14
+ end
15
+
16
+ def puts(message)
17
+ messages << message
18
+ end
19
+
20
+ def write(message)
21
+ messages << message
22
+ end
23
+ end
24
+
25
+ def output
26
+ @output ||= Output.new
27
+ end
28
+
29
+ CLIPPING_FILE = 'test_clippings.txt'
30
+
31
+ Given /^I have a file that contains no clippings$/ do
32
+ File.open(CLIPPING_FILE, 'w') do |f|
33
+ f.write ''
34
+ end
35
+ end
36
+
37
+ When /^I list the books in the file with klipbook$/ do
38
+ File.open(CLIPPING_FILE, 'r') do |f|
39
+ Klipbook::Runner.new(f).list_books(output)
40
+ end
41
+ end
42
+
43
+ Then /^I should see the message "([^"]*)"$/ do |message|
44
+ output.messages.should include(message)
45
+ end
46
+
47
+ Given /^I have a file that contains clippings for the book titled "([^"]*)"$/ do |book_title|
48
+ File.open(CLIPPING_FILE, 'a') do |f|
49
+ f.write <<EOF
50
+ #{book_title}
51
+ - Highlight Loc. 466-69 | Added on Thursday, April 21, 2011, 07:31 AM
52
+
53
+ A test highlight
54
+ ==========
55
+ EOF
56
+ end
57
+ end
58
+
59
+ Given /^I have a file that contains multiple clippings for the book titled "([^"]*)"$/ do |book_title|
60
+ File.open(CLIPPING_FILE, 'a') do |f|
61
+ f.write <<EOF
62
+ #{book_title}
63
+ - Highlight Loc. 466-69 | Added on Thursday, April 21, 2011, 07:31 AM
64
+
65
+ A test highlight
66
+ ==========
67
+ #{book_title}
68
+ - Highlight Loc. 490 | Added on Thursday, April 21, 2011, 07:36 AM
69
+
70
+ A second highlight
71
+ ==========
72
+ EOF
73
+ end
74
+ end
75
+
76
+ When /^I print the summary for book number "([^"]+)" with klipbook/ do |book_number|
77
+ book_number = book_number.to_i
78
+
79
+ File.open(CLIPPING_FILE, 'r') do |f|
80
+ Klipbook::Runner.new(f).print_book_summary(book_number, output)
81
+ end
82
+ end
83
+
84
+ Then /^I should see a pretty summary for the book "([^"]*)"$/ do |book_title|
85
+ output.messages.first.should include("<h1>#{book_title}</h1")
86
+ end
87
+
@@ -0,0 +1,8 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+
4
+ $LOAD_PATH << File.expand_path('../../../lib', __FILE__)
5
+ require 'klipbook'
6
+ require 'fileutils'
7
+
8
+ TEST_DIR = File.expand_path('../../../tmp/test', __FILE__)
@@ -0,0 +1,24 @@
1
+ class String
2
+
3
+ # A string is blank if it's empty or contains whitespaces only:
4
+ #
5
+ # "".blank? # => true
6
+ # " ".blank? # => true
7
+ # " something here ".blank? # => false
8
+ #
9
+ def blank?
10
+ self !~ /\S/
11
+ end
12
+ end
13
+
14
+ class NilClass
15
+
16
+ # +nil+ is blank:
17
+ #
18
+ # nil.blank? # => true
19
+ #
20
+ def blank?
21
+ true
22
+ end
23
+ end
24
+
@@ -0,0 +1,92 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8"/>
5
+ <meta name="generator" content="Klipbook"/>
6
+ <title><%= @clippings.first.title %> - Collated Kindle Clippings</title>
7
+ <style type="text/css">
8
+ body {
9
+ color: #333;
10
+ font-size: 16px;
11
+ line-height: 1.5em;
12
+ font-family: Palatino, Georgia, serif;
13
+ background-color: #fff;
14
+ margin: 0px;
15
+ padding: 20px;
16
+ }
17
+ a {
18
+ text-decoration: none;
19
+ }
20
+ a, a:link, a:visited {
21
+ color: #084ab7;
22
+ }
23
+ a:hover {
24
+ text-decoration: underline;
25
+ }
26
+
27
+ h1 {
28
+ line-height: 1.1em;
29
+ }
30
+
31
+ h2 {
32
+ line-height: 1.1em;
33
+ font-size: 1.3em;
34
+ color: #555;
35
+ font-style: italic;
36
+ }
37
+
38
+ ul {
39
+ margin-top: 2em;
40
+ width: 43em;
41
+ }
42
+
43
+ ul li {
44
+ margin: 1.6em 0;
45
+ list-style: none;
46
+ }
47
+
48
+ ul li p {
49
+ margin-bottom: .5em;
50
+ }
51
+
52
+ ul li footer {
53
+ text-align: right;
54
+ font-size: .85em;
55
+ color: #8C8C8C;
56
+ }
57
+
58
+ footer {
59
+ font-size: .85em;
60
+ margin-left: 20em;
61
+ }
62
+
63
+ footer span {
64
+ font-style: italic;
65
+ }
66
+ </style>
67
+ </head>
68
+ <body>
69
+
70
+ <h1><%= @clippings.first.title %></h1>
71
+ <% unless @author.blank? %>
72
+ <h2>by <%= author %></h2>
73
+ <% end %>
74
+
75
+ <ul>
76
+ <% @clippings.each do |clipping| %>
77
+ <% unless clipping.text.blank? %>
78
+ <li>
79
+ <p>
80
+ <%= ERB::Util.html_escape(clipping.text) %>
81
+ </p>
82
+ <footer><%= clipping.type %><% if clipping.location %> @ loc <%= clipping.location %><% end %></footer>
83
+ </li>
84
+ <% end %>
85
+ <% end %>
86
+ </ul>
87
+
88
+ <footer>
89
+ Generated by <a href="https://github.com/grassdog/klipbook">Klipbook <%= Klipbook::VERSION %></a> on <span><%= DateTime.now.strftime('%e %b %Y at %l:%M %P') %></span>
90
+ </footer>
91
+ </body>
92
+ </html>
@@ -0,0 +1,34 @@
1
+ require 'erb'
2
+
3
+ module Klipbook
4
+ class BookSummary
5
+ attr_accessor :title, :author, :clippings
6
+
7
+ def initialize(title, author, clippings=[])
8
+ @title = title
9
+ @author = author
10
+ @clippings = clippings.sort
11
+ end
12
+
13
+ def hash
14
+ title.hash ^ author.hash
15
+ end
16
+
17
+ def eql?(other)
18
+ self == other
19
+ end
20
+
21
+ def ==(other)
22
+ return false unless other.instance_of?(self.class)
23
+ title == other.title && author == other.author
24
+ end
25
+
26
+ def as_html
27
+ ERB.new(template, 0, '%<>').result(binding)
28
+ end
29
+
30
+ def template
31
+ @template ||= File.read(File.join(File.dirname(__FILE__), 'book_summary.erb'))
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,48 @@
1
+ require 'thor'
2
+
3
+ module Klipbook
4
+ class CLI < Thor
5
+
6
+ desc 'list CLIPPINGS_FILE', 'List the books in the clippings file'
7
+ def list(clippings_file=nil)
8
+ if (clippings_file.nil?)
9
+ puts 'Please provide a CLIPPINGS_FILE'
10
+ exit 1
11
+ end
12
+
13
+ clippings_file = ensure_clippings_file_exists(clippings_file)
14
+
15
+ Klipbook::Runner.new(clippings_file).list_books
16
+ end
17
+
18
+ desc 'summarise CLIPPINGS_FILE BOOK_NUMBER OUTPUT_FILE', 'Output an html summary of the clippings for a book'
19
+ def summarise(clippings_file=nil, book_number=nil, output_file=nil)
20
+ if (clippings_file.nil? or book_number.nil? or output_file.nil?)
21
+ puts 'Please provide a CLIPPINGS_FILE, BOOK_NUMBER, and OUTPUT_FILE'
22
+ exit 1
23
+ end
24
+
25
+ clippings_file = ensure_clippings_file_exists(clippings_file)
26
+
27
+ book_number = book_number.to_i
28
+
29
+ Klipbook::Runner.new(clippings_file).print_book_summary(book_number, File.open(output_file, 'w'))
30
+ end
31
+
32
+ map '-v' => :version
33
+ desc 'version', 'Display Klipbook version'
34
+ def version
35
+ puts "Klipbook #{Klipbook::VERSION}"
36
+ end
37
+
38
+ no_tasks do
39
+ def ensure_clippings_file_exists(clippings_file)
40
+ unless File.exist?(clippings_file)
41
+ $stderr.puts "Clippings file doesn't exist: #{clippings_file}"
42
+ exit 1
43
+ end
44
+ File.open(clippings_file, 'r')
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,15 @@
1
+ require 'ostruct'
2
+ require 'date'
3
+
4
+ module Klipbook
5
+ class Clipping < OpenStruct
6
+ def initialize(attributes)
7
+ super(attributes)
8
+ self.added_on = DateTime.strptime(self.added_on, '%A, %B %d, %Y, %I:%M %p') if self.added_on
9
+ end
10
+
11
+ def <=>(other)
12
+ (self.location || 0) <=> (other.location || 0)
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,50 @@
1
+ module Klipbook
2
+ class ClippingsFile
3
+ attr_accessor :books
4
+
5
+ def initialize(file_text, parser=Klipbook::ClippingsParser.new)
6
+ @books = extract_books_from_file_text(parser, file_text)
7
+ end
8
+
9
+ private
10
+
11
+ def extract_books_from_file_text(parser, file_text)
12
+ clippings = parser.extract_clippings_from(file_text)
13
+
14
+ group_clippings_into_books(clippings)
15
+ end
16
+
17
+ def group_clippings_into_books(clippings)
18
+ books = clippings.inject({}) do |new_hash, clipping|
19
+ new_hash[hash_key_for_book(clipping)] ||= []
20
+ new_hash[hash_key_for_book(clipping)] << clipping
21
+ new_hash
22
+ end
23
+
24
+ books = sort_books_by_title(books)
25
+ books = sort_clippings_by_location(books)
26
+ build_book_summaries(books)
27
+ end
28
+
29
+ def build_book_summaries(books)
30
+ books.values.map { |clippings| Klipbook::BookSummary.new(clippings.first.title, clippings.first.author, clippings) }
31
+ end
32
+
33
+ def hash_key_for_book(clipping)
34
+ "#{clipping.title}#{clipping.author}"
35
+ end
36
+
37
+ def sort_books_by_title(books)
38
+ Hash[books.sort]
39
+ end
40
+
41
+ def sort_clippings_by_location(books)
42
+ books.inject({}) do |new_hash, (k, v)|
43
+ new_hash[k] = v.sort
44
+ new_hash
45
+ end
46
+ end
47
+ end
48
+ end
49
+
50
+