bindery 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +8 -0
- data/Gemfile +2 -0
- data/README.md +126 -0
- data/Rakefile +8 -0
- data/TODO.taskpaper +14 -0
- data/bindery.gemspec +37 -0
- data/examples/ashtray/book.rb +58 -0
- data/examples/trivial/book.rb +27 -0
- data/examples/trivial/chapter_1.xhtml +3 -0
- data/examples/trivial/chapter_2.xhtml +14 -0
- data/lib/bindery.rb +10 -0
- data/lib/bindery/bindery.rb +9 -0
- data/lib/bindery/book.rb +45 -0
- data/lib/bindery/book_builder.rb +101 -0
- data/lib/bindery/chapter.rb +28 -0
- data/lib/bindery/extensions/file.rb +16 -0
- data/lib/bindery/extensions/string.rb +12 -0
- data/lib/bindery/extensions/zip_file.rb +28 -0
- data/lib/bindery/formats/epub.rb +317 -0
- data/lib/bindery/version.rb +3 -0
- data/spec/bindery/bindery_spec.rb +25 -0
- data/spec/bindery/book_builder_spec.rb +90 -0
- data/spec/spec_helper.rb +18 -0
- metadata +156 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,126 @@
|
|
1
|
+
# Bindery
|
2
|
+
|
3
|
+
[Bindery][] is a [Ruby][] library for packaging ebooks.
|
4
|
+
|
5
|
+
Electronic book formats are typically rather simple.
|
6
|
+
An EPUB book, for example, is just HTML, CSS, and some image files packed into a single zip file along with various bits of metadata.
|
7
|
+
But there are numerous tricky details, and a lot of redundancy in the metadata.
|
8
|
+
Bindery aims to simplify the process.
|
9
|
+
|
10
|
+
To use Bindery, you write a simple Ruby program that describes the book's structure and important metadata.
|
11
|
+
This program is also responsible for identifying the HTML files that correspond to the book's chapters; those files might already exist, but the program can generate them on the fly as well.
|
12
|
+
Then, you simply run that program to generate the book.
|
13
|
+
|
14
|
+
Initially, Bindery will support generating the [EPUB][] format.
|
15
|
+
I'll work to add support for other formats when EPUB support is working well.
|
16
|
+
|
17
|
+
## Example
|
18
|
+
|
19
|
+
Here is a quick example of a Bindery program.
|
20
|
+
|
21
|
+
```ruby
|
22
|
+
require 'bindery'
|
23
|
+
require 'active_support'
|
24
|
+
require 'maruku'
|
25
|
+
|
26
|
+
Bindery.book do |b|
|
27
|
+
b.output 'anthology'
|
28
|
+
b.format :epub
|
29
|
+
|
30
|
+
b.title "The Great Elbonian Novel"
|
31
|
+
b.language 'en'
|
32
|
+
b.url 'http://glv.github.com/bindery/books/example'
|
33
|
+
b.author 'Glenn Vanderburg'
|
34
|
+
|
35
|
+
b.frontmatter 'Preface', 'pref.xhtml'
|
36
|
+
|
37
|
+
b.chapter 'Introduction', 'intro.xhtml'
|
38
|
+
|
39
|
+
# process a collection of files called "chapter_1.md"
|
40
|
+
Dir['*.md'].sort.each do |file_name|
|
41
|
+
stem = file_name.sub(/\.md$/, '')
|
42
|
+
output_file_name = "#{stem}.xhtml_gen"
|
43
|
+
system %{markdown <#{file_name} >#{output_file_name}}
|
44
|
+
b.chapter stem.humanize, output_file_name
|
45
|
+
end
|
46
|
+
|
47
|
+
# an alternative way to process chapters, assuming the chapters use
|
48
|
+
# Maruku's metadata support and contain the chapter title. (Maruku
|
49
|
+
# is a Ruby Markdown library that supports numerous common extensions
|
50
|
+
# to the basic Markdown syntax.)
|
51
|
+
Dir['*.maruku'].sort.each do |file_name|
|
52
|
+
b.chapter do
|
53
|
+
output_file_name = file_name.sub(/\.maruku$/, '.xhtml_gen')
|
54
|
+
doc = Maruku.new(IO.read(file_name))
|
55
|
+
open(output_file_name, "w") {|os| os.write(doc.to_html)}
|
56
|
+
# return a hash with title and file info:
|
57
|
+
{ :title => doc.attributes[:title], :file => output_file_name }
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
b.backmatter 'Colophon' # filename assumed to be colophon.xhtml
|
62
|
+
end
|
63
|
+
```
|
64
|
+
|
65
|
+
## Status
|
66
|
+
|
67
|
+
Bindery is currently limited to generating EPUB books that do not contain images or other non-textual content.
|
68
|
+
|
69
|
+
It is also in a very early stage.
|
70
|
+
It's capable of generating very simple books, but many features (including the frontmatter and backmatter methods in the example above) do not work yet.
|
71
|
+
Additionally, validation is sketchy, so it's quite likely that the books you build using Bindery will not be valid EPUB files.
|
72
|
+
But the basics are there, and contributions are welcome.
|
73
|
+
|
74
|
+
Planned features include:
|
75
|
+
|
76
|
+
* options for sections (with and without section title pages) rather than just a flat chapter structure.
|
77
|
+
* additional metadata including all of [Dublin Core][].
|
78
|
+
* support for images
|
79
|
+
* support for multiple stylesheets
|
80
|
+
* support for generating Mobipocket books
|
81
|
+
|
82
|
+
## When to use Bindery
|
83
|
+
|
84
|
+
There are other systems for generating ebooks; [git-scribe][] is one of the best.
|
85
|
+
However most ebook generation systems prescribe a particular authoring format or tool.
|
86
|
+
For example, git-scribe assumes you will be using [AsciiDoc][] and [Git][], and is additionally optimized for use on [GitHub][].
|
87
|
+
Other systems are designed to work with proprietary tools such as [Microsoft Word][] or [Adobe InDesign][].
|
88
|
+
Each such format or tool has strengths and weaknesses.
|
89
|
+
For example, while AsciiDoc is an excellent tool for writing technical books, it's less well suited for novels, memoirs, or poetry.
|
90
|
+
For such tasks (and depending on your personal preferences) it may be better to write in [Markdown][] or [Textile][].
|
91
|
+
|
92
|
+
More importantly, requiring a specific format makes an ebook system difficult to use for a book that includes existing material being republished or repurposed.
|
93
|
+
|
94
|
+
Imagine that, like [Joel Spolsky][], you want to publish an anthology of [your best blog posts][joel on software], or a collection of the [best software writing from around the web][best software writing].
|
95
|
+
Or, like [Steven Johnson][], you write carefully researched books that weave together threads from many fields and disciplines, so you write and collect research in a sophisticated research management tool like [DEVONthink][] (Johnson has written [two][johnson dt1] [articles][johnson dt2] describing his research and writing methods).
|
96
|
+
Or perhaps you simply want to read through the [SproutCore Guides][], but your spare time is fragmented (and usually occurs when you're on an airplane) so you want to convert them to an ebook format so they're always available and your ebook reader will keep track of how far you've read.
|
97
|
+
(For that matter, if you're part of an effort like [SproutCore][] and have great online material like that, Bindery might be the easiest way to package it up into a handy offline format.)
|
98
|
+
|
99
|
+
In any of those cases, the writing will exist in a format that probably doesn't match what existing ebook generation systems expect, and in some cases it will already be in the HTML format that all popular ebook formats are based on.
|
100
|
+
|
101
|
+
Bindery is designed to support those use cases, not just brand new books written with a particular toolchain in mind.
|
102
|
+
If you want to publish in both electronic and paper formats, Bindery is probably not for you.
|
103
|
+
Bindery is targeted at electronic books only.
|
104
|
+
|
105
|
+
[adobe indesign]: http://www.adobe.com/products/indesign.html
|
106
|
+
[asciidoc]: http://www.methods.co.nz/asciidoc/
|
107
|
+
[best software writing]: http://www.apress.com/9781590595008
|
108
|
+
[Bindery]: http://github.com/glv/bindery
|
109
|
+
[devonthink]: http://www.devon-technologies.com/products/devonthink/index.html
|
110
|
+
[dublin core]: http://dublincore.org/sm
|
111
|
+
[epub]: http://idpf.org/epub
|
112
|
+
[git]: http://git-scm.com/
|
113
|
+
[github]: http://github.com/
|
114
|
+
[git-scribe]: http://github.com/schacon/git-scribe
|
115
|
+
[joel on software]: http://www.apress.com/9781590593899
|
116
|
+
[joel spolsky]: http://www.joelonsoftware.com/AboutMe.html
|
117
|
+
[johnson dt1]: http://www.nytimes.com/2005/01/30/books/review/30JOHNSON.html?_r=1&oref=login
|
118
|
+
[johnson dt2]: http://boingboing.net/2009/01/27/diy-how-to-write-a-b.html
|
119
|
+
[markdown]: http://daringfireball.net/projects/markdown/
|
120
|
+
[microsoft word]: http://office.microsoft.com/word/
|
121
|
+
[rake]: http://rake.rubyforge.org/
|
122
|
+
[ruby]: http://ruby-lang.org/
|
123
|
+
[sproutcore]: http://www.sproutcore.com/
|
124
|
+
[sproutcore guides]: http://guides.sproutcore.com/
|
125
|
+
[steven johnson]: http://www.stevenberlinjohnson.com/
|
126
|
+
[textile]: http://www.textism.com/tools/textile/
|
data/Rakefile
ADDED
data/TODO.taskpaper
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
- Flesh out validation
|
2
|
+
Consider moving details to format modules.
|
3
|
+
Investigate Mobi validity constraints to see whether that makes sense.
|
4
|
+
- Add more metadata support
|
5
|
+
- subtitle
|
6
|
+
- different kinds of authors
|
7
|
+
- Title page
|
8
|
+
- Title image
|
9
|
+
- Image support
|
10
|
+
- A basic CSS file
|
11
|
+
- Support for alternative CSS files
|
12
|
+
- Frontmatter
|
13
|
+
- Nested chapter structures
|
14
|
+
- Generate Mobi files
|
data/bindery.gemspec
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "bindery/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "bindery"
|
7
|
+
s.version = Bindery::VERSION
|
8
|
+
s.platform = Gem::Platform::RUBY
|
9
|
+
s.authors = ["Glenn Vanderburg"]
|
10
|
+
s.email = ["glv@vanderburg.org"]
|
11
|
+
s.homepage = "http://github.com/glv/bindery"
|
12
|
+
s.summary = %q{Easy ebook packaging with Ruby}
|
13
|
+
s.description = %q{Bindery is a Ruby library for easy packaging of ebooks.
|
14
|
+
You supply the chapter content (in HTML format) and explain the book's structure to bindery,
|
15
|
+
and bindery generates the various other files required by ebook formats and assembles them
|
16
|
+
into a completed book suitable for installation on an ebook reader.}
|
17
|
+
|
18
|
+
s.rubyforge_project = "bindery"
|
19
|
+
|
20
|
+
s.files = `git ls-files`.split("\n")
|
21
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
22
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
23
|
+
s.require_paths = ["lib"]
|
24
|
+
|
25
|
+
add_runtime_dependency = if s.respond_to?(:specification_version) && Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
26
|
+
:add_runtime_dependency
|
27
|
+
else
|
28
|
+
:add_dependency
|
29
|
+
end
|
30
|
+
s.send(add_runtime_dependency, 'builder')
|
31
|
+
s.send(add_runtime_dependency, 'nokogiri')
|
32
|
+
s.send(add_runtime_dependency, 'zip')
|
33
|
+
|
34
|
+
s.add_development_dependency 'mocha'
|
35
|
+
s.add_development_dependency 'rake'
|
36
|
+
s.add_development_dependency 'rspec', ['~> 2.0']
|
37
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'bindery'
|
2
|
+
require 'open-uri'
|
3
|
+
require 'nokogiri'
|
4
|
+
require 'tidy_ffi'
|
5
|
+
|
6
|
+
# In March, 2011, Errol Morris published on the New York Times website
|
7
|
+
# a five-part reminiscence of a long-ago encounter with famed philosopher
|
8
|
+
# Thomas Kuhn. It's a perfect example of the kind of thing Bindery was
|
9
|
+
# designed for. You could always use Instapaper to allow you to read it
|
10
|
+
# later, but the fact that it's in five pieces means that it wouldn't
|
11
|
+
# appear as a single piece of writing in the Instapaper apps; additionally,
|
12
|
+
# it's long enough that you might well have to stop reading in the middle
|
13
|
+
# of one of the articles, and Instapaper doesn't remember your stopping
|
14
|
+
# point or synch it between devices. (Not to mention that it doesn't let
|
15
|
+
# you highlight or make notes.) An ebook is simply a better format for some
|
16
|
+
# kinds of writing.
|
17
|
+
#
|
18
|
+
# Note: I occasionally write Bindery scripts like this to bundle writing
|
19
|
+
# from the web into a more convenient form for my own personal use. In
|
20
|
+
# that respect, I find it no different (as a matter of copyright) than
|
21
|
+
# using a service like Instapaper. However, it would definitely be a
|
22
|
+
# violation of Morris' and the New York Times' copyrights to distribute
|
23
|
+
# the resulting ebook to others. Don't do that!
|
24
|
+
|
25
|
+
PARTS = [
|
26
|
+
'http://opinionator.blogs.nytimes.com/2011/03/06/the-ashtray-the-ultimatum-part-1/',
|
27
|
+
'http://opinionator.blogs.nytimes.com/2011/03/07/the-ashtray-shifting-paradigms-part-2/',
|
28
|
+
'http://opinionator.blogs.nytimes.com/2011/03/08/the-ashtray-hippasus-of-metapontum-part-3/',
|
29
|
+
'http://opinionator.blogs.nytimes.com/2011/03/09/the-ashtray-the-author-of-the-quixote-part-4/',
|
30
|
+
'http://opinionator.blogs.nytimes.com/2011/03/10/the-ashtray-this-contest-of-interpretation-part-5/'
|
31
|
+
]
|
32
|
+
|
33
|
+
def process_part(url, i)
|
34
|
+
file_name = "chapter_#{i+1}.xhtml_gen"
|
35
|
+
doc = Nokogiri::HTML(open(url))
|
36
|
+
#puts doc
|
37
|
+
title = doc.at_css('h1.entry-title').text.sub(/^.*?: (.*) \(Part \d\)\s*$/, '\\1')
|
38
|
+
open(file_name, 'w') do |os|
|
39
|
+
content = doc.at_css('div.entry-content')
|
40
|
+
content.children[0..1].each{|c| c.remove}
|
41
|
+
os.write content.serialize
|
42
|
+
end
|
43
|
+
{ :title => title, :file => file_name }
|
44
|
+
end
|
45
|
+
|
46
|
+
Bindery.book do |b|
|
47
|
+
b.output 'the_ashtray'
|
48
|
+
b.format :epub
|
49
|
+
|
50
|
+
b.title "The Ashtray"
|
51
|
+
b.author 'Errol Morris'
|
52
|
+
b.url 'http://glenn.mp/book/the_ashtray'
|
53
|
+
b.language 'en'
|
54
|
+
|
55
|
+
PARTS.each_with_index do |url, i|
|
56
|
+
b.chapter { process_part(url, i) }
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'bindery'
|
2
|
+
|
3
|
+
Bindery.book do |b|
|
4
|
+
b.output 'trivial'
|
5
|
+
b.format :epub
|
6
|
+
|
7
|
+
b.title "A Trivial Bindery Example"
|
8
|
+
b.author 'Glenn Vanderburg'
|
9
|
+
b.url 'http://glenn.mp/book/trivial_example'
|
10
|
+
b.language 'en'
|
11
|
+
|
12
|
+
b.chapter "Chapter 1", 'chapter_1.xhtml'
|
13
|
+
b.chapter "Chapter 2", 'chapter_2.xhtml', :body_only => false
|
14
|
+
b.chapter "Chapter 3" do
|
15
|
+
# It's good practice to have a way to distinguish source files from
|
16
|
+
# those generated as part of the build, so you can have a Rake task
|
17
|
+
# or something to clean up the intermediate files.
|
18
|
+
"chapter_3.xhtml_gen".tap do |filename|
|
19
|
+
File.open(filename, "w") do |os|
|
20
|
+
os.write %{
|
21
|
+
<p>Let's just write this chapter in line, shall we?</p>
|
22
|
+
<p>It seems easier that way, when the chapters are so short.</p>
|
23
|
+
}
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8" ?>
|
2
|
+
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
|
3
|
+
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
|
4
|
+
<head>
|
5
|
+
<meta http-equiv="Content-Type" content="application/xhtml+xml; charset=utf-8" />
|
6
|
+
<title>Chapter 2</title>
|
7
|
+
<link rel="stylesheet" href="css/book.css" type="text/css" />
|
8
|
+
</head>
|
9
|
+
<body>
|
10
|
+
<h2>Chapter 2</h2>
|
11
|
+
|
12
|
+
<p>Unlike Chapter 1, this chapter's source file is a full, valid XHTML file, not just the body.</p>
|
13
|
+
</body>
|
14
|
+
</html>
|
data/lib/bindery.rb
ADDED
data/lib/bindery/book.rb
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
module Bindery
|
2
|
+
class Book
|
3
|
+
attr_accessor :output, :url, :isbn, :title, :language, :author, :subtitle
|
4
|
+
|
5
|
+
def formats
|
6
|
+
@formats ||= []
|
7
|
+
end
|
8
|
+
|
9
|
+
def chapters
|
10
|
+
@chapters ||= []
|
11
|
+
end
|
12
|
+
|
13
|
+
def full_title
|
14
|
+
title + (subtitle ? ": #{subtitle}" : '')
|
15
|
+
end
|
16
|
+
|
17
|
+
def valid?
|
18
|
+
configuration_valid? && metadata_valid? && chapters_valid?
|
19
|
+
end
|
20
|
+
|
21
|
+
def configuration_valid?
|
22
|
+
true
|
23
|
+
# formats specified or correctly defaulted
|
24
|
+
# ouput specified
|
25
|
+
# at least one chapter
|
26
|
+
end
|
27
|
+
|
28
|
+
def metadata_valid?
|
29
|
+
true
|
30
|
+
# everything required has been specified (must find out what that is)
|
31
|
+
# what is there is correct
|
32
|
+
end
|
33
|
+
|
34
|
+
def chapters_valid?
|
35
|
+
chapters.all?{|chapter| chapter.valid?} # && chapter file names are unique
|
36
|
+
end
|
37
|
+
|
38
|
+
def generate
|
39
|
+
formats.each do |format|
|
40
|
+
require "bindery/formats/#{format}"
|
41
|
+
::Bindery::Formats.const_get(format.to_s.capitalize).new(self).generate
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
module Bindery
|
2
|
+
|
3
|
+
class BookBuilder
|
4
|
+
attr_accessor :book
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
self.book = ::Bindery::Book.new
|
8
|
+
end
|
9
|
+
|
10
|
+
def format(fmt)
|
11
|
+
raise "unsupported format :#{fmt}" unless [:epub].include?(fmt)
|
12
|
+
book.formats << fmt
|
13
|
+
end
|
14
|
+
|
15
|
+
def output(basename)
|
16
|
+
raise "output already set to #{book.output}" unless book.output.nil?
|
17
|
+
book.output = basename.to_s
|
18
|
+
end
|
19
|
+
|
20
|
+
def url(url)
|
21
|
+
book.url = url
|
22
|
+
end
|
23
|
+
|
24
|
+
def isbn(isbn)
|
25
|
+
book.isbn = isbn
|
26
|
+
end
|
27
|
+
|
28
|
+
def title(title)
|
29
|
+
book.title = title
|
30
|
+
end
|
31
|
+
|
32
|
+
def subtitle(subtitle)
|
33
|
+
book.subtitle = subtitle
|
34
|
+
end
|
35
|
+
|
36
|
+
def language(language)
|
37
|
+
book.language = language
|
38
|
+
end
|
39
|
+
|
40
|
+
def author(author)
|
41
|
+
book.author = author
|
42
|
+
end
|
43
|
+
|
44
|
+
# :call-seq:
|
45
|
+
# chapter(title, filename, options={})
|
46
|
+
# chapter(title, options={}) { ... }
|
47
|
+
# chapter(options={}) { ... }
|
48
|
+
#
|
49
|
+
# Add a chapter to the book.
|
50
|
+
#
|
51
|
+
# If called with a title and a filename, the chapter's content should
|
52
|
+
# be found in the named file.
|
53
|
+
#
|
54
|
+
# If called with a title and a block, the block should generate or
|
55
|
+
# retrieve the chapter's content, write it to a file, and return the
|
56
|
+
# file name.
|
57
|
+
#
|
58
|
+
# If called with no parameters and a block, the block should generate
|
59
|
+
# or retrieve the chapter's content, write it to a file, and return a
|
60
|
+
# hash with the following keys:
|
61
|
+
# [:title] the chapter title (required)
|
62
|
+
# [:file] the name of the file containing the chapter content (required)
|
63
|
+
#
|
64
|
+
# An options hash parameter is always allowed, and the following
|
65
|
+
# options are supported:
|
66
|
+
# [:body_only] the file contains only the body of the XHTML document,
|
67
|
+
# and Bindery should wrap it to create a valid document.
|
68
|
+
# Defaults to true.
|
69
|
+
def chapter(*args)
|
70
|
+
default_options = {:body_only => true}
|
71
|
+
options = default_options.merge(args.last.kind_of?(Hash) ? args.pop : {})
|
72
|
+
if block_given?
|
73
|
+
chapter_dynamic(options, *args){yield}
|
74
|
+
else
|
75
|
+
chapter_static(options, *args)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
protected
|
80
|
+
|
81
|
+
def chapter_static(options, *args)
|
82
|
+
title, file = args
|
83
|
+
raise ArgumentError, "title not specified" if title.nil?
|
84
|
+
raise ArgumentError, "file not specified" if file.nil?
|
85
|
+
book.chapters << Chapter.new(title, file, options)
|
86
|
+
end
|
87
|
+
|
88
|
+
def chapter_dynamic(options, title=nil)
|
89
|
+
if title
|
90
|
+
file = yield
|
91
|
+
raise "expected the block to return a filename string" unless file.kind_of?(String)
|
92
|
+
chapter_static(options, title, yield)
|
93
|
+
else
|
94
|
+
info = yield
|
95
|
+
raise "expected the block to return a hash containing :title, :file, etc." unless info.kind_of?(Hash)
|
96
|
+
chapter_static(options, info[:title], info[:file])
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Bindery
|
2
|
+
class Chapter
|
3
|
+
attr_accessor :file
|
4
|
+
attr_accessor :title
|
5
|
+
attr_accessor :options
|
6
|
+
|
7
|
+
def initialize(title, file, options)
|
8
|
+
self.title = title
|
9
|
+
self.file = file
|
10
|
+
self.options = options
|
11
|
+
end
|
12
|
+
|
13
|
+
def valid?
|
14
|
+
true
|
15
|
+
# title specified
|
16
|
+
# file exists, readable
|
17
|
+
# file content properly formed? Does that matter? Can we verify it?
|
18
|
+
end
|
19
|
+
|
20
|
+
def body_only?
|
21
|
+
options.fetch(:body_only, true)
|
22
|
+
end
|
23
|
+
|
24
|
+
def include_images?
|
25
|
+
options.fetch(:include_images, true)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Bindery
|
2
|
+
module Extensions
|
3
|
+
module FileClassMethods
|
4
|
+
def base_parts(fn)
|
5
|
+
ext = File.extname(fn)
|
6
|
+
[File.basename(fn, ext), ext]
|
7
|
+
end
|
8
|
+
|
9
|
+
def stemname(fn)
|
10
|
+
base_parts(fn)[0]
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
File.extend(Bindery::Extensions::FileClassMethods)
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'zip'
|
2
|
+
|
3
|
+
module Bindery
|
4
|
+
module Extensions
|
5
|
+
module ZipFileExtensions
|
6
|
+
def write_file(entry, contents)
|
7
|
+
get_output_stream(entry){|os| os.write(contents) }
|
8
|
+
end
|
9
|
+
|
10
|
+
def write_uncompressed_file(entry_name, contents)
|
11
|
+
entry = Zip::ZipEntry.new(@name, entry_name.to_s)
|
12
|
+
entry.compression_method = Zip::ZipEntry::STORED
|
13
|
+
write_file(entry, contents)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
module ZipStreamableStreamExtensions
|
18
|
+
def kind_of?(thing)
|
19
|
+
# ZipStreamableStream is a ZipEntry, but through delegation, not inheritance.
|
20
|
+
return true if thing == ::Zip::ZipEntry
|
21
|
+
super
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
Zip::ZipFile.send(:include, Bindery::Extensions::ZipFileExtensions)
|
28
|
+
Zip::ZipStreamableStream.send(:include, Bindery::Extensions::ZipStreamableStreamExtensions)
|
@@ -0,0 +1,317 @@
|
|
1
|
+
require 'builder'
|
2
|
+
require 'zip'
|
3
|
+
require 'bindery/extensions/zip_file'
|
4
|
+
require 'nokogiri'
|
5
|
+
|
6
|
+
module Bindery
|
7
|
+
module Formats
|
8
|
+
|
9
|
+
# Builds an EPUB book file from the book description.
|
10
|
+
#
|
11
|
+
# The {EPUB Wikipedia entry}[http://en.wikipedia.org/wiki/EPUB] provides a nice, concise overview of the EPUB format.
|
12
|
+
#
|
13
|
+
# For more precise details:
|
14
|
+
# * The overall structure of an EPUB file is documented in
|
15
|
+
# {Open Container Format (OCF) 2.0.1 - Recommended Specification}[http://idpf.org/epub/20/spec/OCF_2.0.1_draft.doc].
|
16
|
+
# * The format of the OPF file is documented in
|
17
|
+
# {Open Packaging Format (OPF) 2.0.1 - Recommended Specification}[http://idpf.org/epub/20/spec/OPF_2.0.1_draft.htm].
|
18
|
+
# * The format of the NCX file is documented in
|
19
|
+
# {Section 8 of "Specifications for the Digital Talking Book"}[http://www.niso.org/workrooms/daisy/Z39-86-2005.html#NCX].
|
20
|
+
# * Details of the format of other files allowed in EPUB documents are found in
|
21
|
+
# {Open Publication Structure (OPS) 2.0.1 - Recommended Specification}[http://idpf.org/epub/20/spec/OPS_2.0.1_draft.htm].
|
22
|
+
class Epub
|
23
|
+
|
24
|
+
MimeTypes = {
|
25
|
+
'.jpg' => 'image/jpeg',
|
26
|
+
'.png' => 'image/png',
|
27
|
+
'.gif' => 'image/gif',
|
28
|
+
}
|
29
|
+
|
30
|
+
class ManifestEntry < Struct.new(:file_name, :xml_id, :mime_type)
|
31
|
+
end
|
32
|
+
|
33
|
+
attr_accessor :book, :manifest_entries
|
34
|
+
|
35
|
+
def initialize(book)
|
36
|
+
self.book = book
|
37
|
+
book.extend BookMethods
|
38
|
+
book.chapters.each{|chapter| chapter.extend ChapterMethods}
|
39
|
+
self.manifest_entries = []
|
40
|
+
end
|
41
|
+
|
42
|
+
def generate
|
43
|
+
File.delete(book.epub_output_file) if File.exist?(book.epub_output_file)
|
44
|
+
Zip::ZipFile.open(book.epub_output_file, Zip::ZipFile::CREATE) do |zipfile|
|
45
|
+
# FIXME: The mimetype file is supposed to be the first one in the Zip directory, but that doesn't seem to be happening.
|
46
|
+
zipfile.write_uncompressed_file 'mimetype', mimetype
|
47
|
+
zipfile.mkdir 'META-INF'
|
48
|
+
zipfile.write_file 'META-INF/container.xml', container
|
49
|
+
|
50
|
+
# also frontmatter, backmatter
|
51
|
+
book.chapters.each do |chapter|
|
52
|
+
write_chapter(chapter, zipfile)
|
53
|
+
end
|
54
|
+
|
55
|
+
zipfile.mkdir 'css'
|
56
|
+
zipfile.write_file 'css/book.css', stylesheet
|
57
|
+
|
58
|
+
zipfile.write_file 'book.opf', opf
|
59
|
+
zipfile.write_file 'book.ncx', ncx
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def mimetype
|
64
|
+
# the mimetype file must be the first file in the archive
|
65
|
+
# it must be ASCII, uncompressed, and unencrypted
|
66
|
+
'application/epub+zip'
|
67
|
+
end
|
68
|
+
|
69
|
+
def container
|
70
|
+
%q{|<?xml version="1.0" encoding="UTF-8" ?>
|
71
|
+
|<container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
|
72
|
+
| <rootfiles>
|
73
|
+
| <rootfile full-path="book.opf" media-type="application/oebps-package+xml"/>
|
74
|
+
| </rootfiles>
|
75
|
+
|</container>
|
76
|
+
|}.strip_margin
|
77
|
+
end
|
78
|
+
|
79
|
+
def opf
|
80
|
+
xm = Builder::XmlMarkup.new(:indent => 2)
|
81
|
+
xm.instruct!
|
82
|
+
xm.package('version'=>'2.0', 'xmlns'=>'http://www.idpf.org/2007/opf', 'unique-identifier'=>'BookId') {
|
83
|
+
|
84
|
+
xm.metadata('xmlns:dc'=>'http://purl.org/dc/elements/1.1/', 'xmlns:opf'=>'http://www.idpf.org/2007/opf') {
|
85
|
+
# required elements
|
86
|
+
xm.dc :title, book.full_title
|
87
|
+
xm.dc :language, book.language
|
88
|
+
xm.dc :identifier, book.url, ident_options('opf:scheme'=>'URL') if book.url
|
89
|
+
xm.dc :identifier, book.isbn, ident_options('opf:scheme'=>'ISBN') if book.isbn
|
90
|
+
|
91
|
+
# optional elements
|
92
|
+
xm.dc :creator, book.author, 'opf:role'=>'aut' if book.author
|
93
|
+
}
|
94
|
+
|
95
|
+
xm.manifest {
|
96
|
+
book.chapters.each{|chapter| xm.item 'id'=>chapter.epub_id, 'href'=>chapter.epub_output_file, 'media-type'=>'application/xhtml+xml'}
|
97
|
+
# also frontmatter, backmatter
|
98
|
+
xm.item 'id'=>'stylesheet', 'href'=>'css/book.css', 'media-type'=>'text/css'
|
99
|
+
manifest_entries.each do |entry|
|
100
|
+
xm.item 'id'=>entry.xml_id, 'href'=>entry.file_name, 'media-type'=>entry.mime_type
|
101
|
+
end
|
102
|
+
# xm.item 'id'=>'myfont', 'href'=>'css/myfont.otf', 'media-type'=>'application/x-font-opentype'
|
103
|
+
xm.item 'id'=>'ncx', 'href'=>'book.ncx', 'media-type'=>'application/x-dtbncx+xml'
|
104
|
+
}
|
105
|
+
|
106
|
+
xm.spine('toc'=>'ncx') {
|
107
|
+
book.chapters.each{|chapter| xm.itemref 'idref'=>chapter.epub_id}
|
108
|
+
}
|
109
|
+
|
110
|
+
# xm.guide {
|
111
|
+
# xm.reference 'type'='loi', 'title'=>'List of Illustrations', 'href'=>'appendix.html#figures'
|
112
|
+
# }
|
113
|
+
}
|
114
|
+
end
|
115
|
+
|
116
|
+
def ncx
|
117
|
+
xm = Builder::XmlMarkup.new(:indent => 2)
|
118
|
+
xm.instruct!
|
119
|
+
xm.declare!(:DOCTYPE, :ncx, :PUBLIC, '-//NISO//DTD ncx 2005-1//EN', 'http://www.daisy.org/z3986/2005/ncx-2005-1.dtd')
|
120
|
+
xm.ncx('version'=>'2005-1', 'xml:lang'=>'en', 'xmlns'=>'http://www.daisy.org/z3986/2005/ncx/') {
|
121
|
+
xm.head {
|
122
|
+
xm.meta 'name'=>'dtb:uid', 'content'=>book.ident
|
123
|
+
xm.meta 'name'=>'dtb:depth', 'content'=>book.depth
|
124
|
+
xm.meta 'name'=>'dtb:totalPageCount', 'content'=>0
|
125
|
+
xm.meta 'name'=>'dtb:maxPageNumber', 'content'=>0
|
126
|
+
}
|
127
|
+
|
128
|
+
xm.docTitle {
|
129
|
+
xm.text book.full_title
|
130
|
+
}
|
131
|
+
|
132
|
+
xm.docAuthor {
|
133
|
+
xm.text book.author
|
134
|
+
}
|
135
|
+
|
136
|
+
xm.navMap {
|
137
|
+
play_order = 1
|
138
|
+
|
139
|
+
# also frontmatter, backmatter
|
140
|
+
book.chapters.each do |chapter|
|
141
|
+
xm.navPoint('class'=>'chapter', 'id'=>chapter.epub_id, 'playOrder'=>play_order) {
|
142
|
+
xm.navLabel {
|
143
|
+
xm.text chapter.title
|
144
|
+
}
|
145
|
+
xm.content 'src'=>chapter.epub_output_file
|
146
|
+
}
|
147
|
+
play_order += 1
|
148
|
+
end
|
149
|
+
}
|
150
|
+
}
|
151
|
+
end
|
152
|
+
|
153
|
+
def write_chapter(chapter, zipfile)
|
154
|
+
save_options = Nokogiri::XML::Node::SaveOptions
|
155
|
+
File.open(chapter.file, 'r:UTF-8') do |ch_in|
|
156
|
+
doc = Nokogiri.HTML(ch_in.read)
|
157
|
+
include_images(doc, zipfile) if chapter.include_images?
|
158
|
+
zipfile.get_output_stream(chapter.epub_output_file) do |ch_out|
|
159
|
+
if chapter.body_only?
|
160
|
+
# FIXME: must HTML-escape the chapter title
|
161
|
+
ch_out.write %{|<?xml version="1.0" encoding="UTF-8" ?>
|
162
|
+
|<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
|
163
|
+
|<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
|
164
|
+
|<head>
|
165
|
+
| <meta http-equiv="Content-Type" content="application/xhtml+xml; charset=utf-8" />
|
166
|
+
| <title>#{chapter.title}</title>
|
167
|
+
| <link rel="stylesheet" href="css/book.css" type="text/css" />
|
168
|
+
|</head>
|
169
|
+
|}.strip_margin
|
170
|
+
ch_out.write doc.at('body').serialize(:save_with => (save_options::AS_XHTML | save_options::NO_DECLARATION))
|
171
|
+
ch_out.write %{|</html>
|
172
|
+
|}.strip_margin
|
173
|
+
else
|
174
|
+
ch_out.write doc.serialize(:save_with => save_options::AS_XHTML)
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
def include_images(doc, zipfile)
|
181
|
+
# TODO: where else can images appear? Style sheets?
|
182
|
+
zipfile.mkdir('images') unless zip_dir_exists?(zipfile, 'images')
|
183
|
+
doc.css('img').each do |img|
|
184
|
+
url = img['src']
|
185
|
+
img_fn = make_image_file_name(zipfile, url)
|
186
|
+
# TODO: These images should be cached somewhere for multi-format runs
|
187
|
+
open(url, 'r') do |is|
|
188
|
+
zipfile.get_output_stream(img_fn) do |os|
|
189
|
+
os.write is.read
|
190
|
+
end
|
191
|
+
end
|
192
|
+
add_manifest_entry(img_fn)
|
193
|
+
img['src'] = img_fn
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
def add_manifest_entry(file_name)
|
198
|
+
xml_id, ext = File.base_parts(file_name.gsub('/', '-'))
|
199
|
+
manifest_entries << ManifestEntry.new(file_name, xml_id, MimeTypes[ext])
|
200
|
+
end
|
201
|
+
|
202
|
+
def cover
|
203
|
+
xm = Builder::XmlMarkup.new(:indent => 2)
|
204
|
+
xm.instruct!
|
205
|
+
xm.declare!(:DOCTYPE, :html, :PUBLIC, '-//W3C//DTD XHTML 1.1//END', 'http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd')
|
206
|
+
xm.html('xmlns'=>'http://www.w3.org/1999/xhtml') { # ??? xml:lang attribute?
|
207
|
+
xm.head {
|
208
|
+
xm.title "#{book.title}: Cover"
|
209
|
+
xm.meta('http-equiv'=>'Content-Type', 'content'=>'application/xhtml+xml; charset=utf-8')
|
210
|
+
}
|
211
|
+
xm.body {
|
212
|
+
xm.div('style'=>'text-align: center; page-break-after: always;') {
|
213
|
+
if book.cover
|
214
|
+
xm.img('src'=>"images/#{book.cover}", 'alt'=>book.title, 'style'=>'height: 100%; max-width: 100%;')
|
215
|
+
else
|
216
|
+
xm.h1 book.title
|
217
|
+
xm.h2 book.subtitle if book.subtitle
|
218
|
+
xm.h3 "by #{book.author}" if book.author
|
219
|
+
end
|
220
|
+
}
|
221
|
+
}
|
222
|
+
}
|
223
|
+
end
|
224
|
+
|
225
|
+
def stylesheet
|
226
|
+
# This is a start, but needs work.
|
227
|
+
%q{|@page {
|
228
|
+
| margin-top: 0.8em;
|
229
|
+
| margin-bottom: 0.8em;}
|
230
|
+
|
|
231
|
+
|body {
|
232
|
+
| margin-left: 1em;
|
233
|
+
| margin-right: 1em;
|
234
|
+
| padding: 0;}
|
235
|
+
|
|
236
|
+
|h2 {
|
237
|
+
| padding-top:0;
|
238
|
+
| display:block;}
|
239
|
+
|
|
240
|
+
|p {
|
241
|
+
| margin-top: 0.3em;
|
242
|
+
| margin-bottom: 0.3em;
|
243
|
+
| text-indent: 1.0em;
|
244
|
+
| text-align: justify;}
|
245
|
+
|
|
246
|
+
|body > p:first-child {text-indent: 0}
|
247
|
+
|div.text p:first-child {text-indent: 0}
|
248
|
+
|
|
249
|
+
|blockquote p, li p {
|
250
|
+
| text-indent: 0.0em;
|
251
|
+
| text-align: left;}
|
252
|
+
|
|
253
|
+
|div.chapter {padding-top: 3.0em;}
|
254
|
+
|div.part {padding-top: 3.0em;}
|
255
|
+
|h3.section_title {text-align: center;}
|
256
|
+
|}.strip_margin
|
257
|
+
end
|
258
|
+
|
259
|
+
def ident_options(opts)
|
260
|
+
if book.isbn
|
261
|
+
return opts.merge('id'=>'BookId') if opts['opf:scheme'] == 'ISBN'
|
262
|
+
else
|
263
|
+
return opts.merge('id'=>'BookId') if opts['opf:scheme'] == 'URL'
|
264
|
+
end
|
265
|
+
opts
|
266
|
+
end
|
267
|
+
|
268
|
+
def zip_dir_exists?(zipfile, dirname)
|
269
|
+
dirname = "#{dirname}/" unless dirname =~ %r{/$}
|
270
|
+
zipfile.entries.any?{|e| e.directory? && e.name == dirname}
|
271
|
+
end
|
272
|
+
|
273
|
+
def zip_file_exists?(zipfile, filename)
|
274
|
+
zipfile.entries.any?{|e| e.name == filename}
|
275
|
+
end
|
276
|
+
|
277
|
+
def make_image_file_name(zipfile, url)
|
278
|
+
stem, ext = File.base_parts(url)
|
279
|
+
filename = "images/#{stem}#{ext}"
|
280
|
+
n = 0
|
281
|
+
while zip_file_exists?(zipfile, filename)
|
282
|
+
n += 1
|
283
|
+
filename = "#{stem}_#{n}#{ext}"
|
284
|
+
end
|
285
|
+
filename
|
286
|
+
end
|
287
|
+
|
288
|
+
module BookMethods
|
289
|
+
def epub_output_file
|
290
|
+
@epub_output_file ||= "#{output}.epub"
|
291
|
+
end
|
292
|
+
|
293
|
+
def depth
|
294
|
+
1
|
295
|
+
end
|
296
|
+
|
297
|
+
def ident
|
298
|
+
isbn || url
|
299
|
+
end
|
300
|
+
end
|
301
|
+
|
302
|
+
module ChapterMethods
|
303
|
+
def epub_id
|
304
|
+
@epub_id ||= File.stemname(file)
|
305
|
+
end
|
306
|
+
|
307
|
+
def epub_output_file
|
308
|
+
@epub_output_file ||= "#{epub_id}.xhtml"
|
309
|
+
end
|
310
|
+
end
|
311
|
+
end
|
312
|
+
end
|
313
|
+
end
|
314
|
+
|
315
|
+
# Notes:
|
316
|
+
# cover image file: in manifest, then in metadata as <meta name="cover" content="manifest-entry-id"/>
|
317
|
+
# cover: xhtml file, in manifest, also in spine as <itemref idref="manifest-entry-id" linear="no"/>, also in guide as <reference type="cover" title="Cover" href="cover.xhtml"/> (why does that use a direct href instead of an id ref?)
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Bindery do
|
4
|
+
describe ".book" do
|
5
|
+
it "yields a new BookBuilder object to the block" do
|
6
|
+
Bindery.book do |b|
|
7
|
+
b.should be_kind_of Bindery::BookBuilder
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
it "generates the book after the block returns if the book is valid" do
|
12
|
+
Bindery.book do |b|
|
13
|
+
b.book.expects(:valid?).once.returns(true)
|
14
|
+
b.book.expects(:generate).once
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
it "does not generate the book if the book is not valid" do
|
19
|
+
Bindery.book do |b|
|
20
|
+
b.book.expects(:valid?).once.returns(false)
|
21
|
+
b.book.expects(:generate).never
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Bindery::BookBuilder do
|
4
|
+
let(:book) { subject.book }
|
5
|
+
|
6
|
+
describe "#book" do
|
7
|
+
it "returns the configured Book instance" do
|
8
|
+
book.should be_kind_of(Bindery::Book)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
describe "#output" do
|
13
|
+
it "stores the filename to use (minus suffix) for output book files" do
|
14
|
+
subject.output 'foo'
|
15
|
+
book.output.should == 'foo'
|
16
|
+
end
|
17
|
+
|
18
|
+
it "converts its argument to a string before storing it" do
|
19
|
+
subject.output :foo
|
20
|
+
book.output.should == 'foo'
|
21
|
+
end
|
22
|
+
|
23
|
+
it "raises an error if the output has already been set" do
|
24
|
+
subject.output 'bar'
|
25
|
+
expect { subject.output 'foo' }.to raise_error
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
describe "#format" do
|
30
|
+
it "raises an exception if the supplied format is unsupported" do
|
31
|
+
expect { subject.format :interpress }.to raise_error
|
32
|
+
expect { subject.format :mobi }.to raise_error
|
33
|
+
end
|
34
|
+
|
35
|
+
it "adds the supplied format to the list of formats to generate for this book" do
|
36
|
+
subject.format :epub
|
37
|
+
book.formats.should =~ [:epub]
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
describe "#chapter" do
|
42
|
+
|
43
|
+
context "when called without a block" do
|
44
|
+
it "raises an exception if title or filename are not supplied" do
|
45
|
+
expect { subject.chapter }.to raise_error(ArgumentError, "title not specified")
|
46
|
+
expect { subject.chapter "A" }.to raise_error(ArgumentError, "file not specified")
|
47
|
+
end
|
48
|
+
|
49
|
+
it "stores a new chapter with the supplied options" do
|
50
|
+
subject.chapter "Foo", "bar.xhtml"
|
51
|
+
book.chapters.should have(1).elements
|
52
|
+
book.chapters.first.title.should == "Foo"
|
53
|
+
book.chapters.first.file.should == "bar.xhtml"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
context "when called with a block" do
|
58
|
+
|
59
|
+
context "and title is supplied as an argument" do
|
60
|
+
it "expects the block to result in a filename string" do
|
61
|
+
expect { subject.chapter("Foo"){ 3 } }.to raise_error
|
62
|
+
end
|
63
|
+
|
64
|
+
it "stores the title and the filename as a new chapter" do
|
65
|
+
subject.chapter("Foo"){ 'bar.xhtml' }
|
66
|
+
book.chapters.should have(1).elements
|
67
|
+
book.chapters.first.title.should == "Foo"
|
68
|
+
book.chapters.first.file.should == "bar.xhtml"
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
context "and title is not supplied as an argument" do
|
73
|
+
it "expects the block to result in a hash with :title and :file options" do
|
74
|
+
expect { subject.chapter{ "Invalid" } }.to raise_error
|
75
|
+
expect { subject.chapter{ {:title => 'Foo'} } }.to raise_error
|
76
|
+
expect { subject.chapter{ {:file => 'bar.xhtml'} } }.to raise_error
|
77
|
+
end
|
78
|
+
|
79
|
+
it "stores the title and the filename as a new chapter" do
|
80
|
+
subject.chapter { {:title => 'Foo', :file => 'bar.xhtml'} }
|
81
|
+
book.chapters.should have(1).elements
|
82
|
+
book.chapters.first.title.should == "Foo"
|
83
|
+
book.chapters.first.file.should == "bar.xhtml"
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
90
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'rspec/core'
|
2
|
+
|
3
|
+
require 'bindery'
|
4
|
+
|
5
|
+
Dir['./spec/support/**/*.rb'].map {|f| require f}
|
6
|
+
|
7
|
+
def in_editor?
|
8
|
+
ENV.has_key?('TM_MODE') || ENV.has_key?('EMACS') || ENV.has_key?('VIM')
|
9
|
+
end
|
10
|
+
|
11
|
+
RSpec.configure do |c|
|
12
|
+
c.color_enabled = !in_editor?
|
13
|
+
c.filter_run :focus => true
|
14
|
+
c.mock_with :mocha
|
15
|
+
c.run_all_when_everything_filtered = true
|
16
|
+
c.add_formatter :progress
|
17
|
+
c.add_formatter :documentation, 'rspec.txt'
|
18
|
+
end
|
metadata
ADDED
@@ -0,0 +1,156 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: bindery
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease:
|
5
|
+
version: 0.0.2
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Glenn Vanderburg
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
|
13
|
+
date: 2011-06-18 00:00:00 -05:00
|
14
|
+
default_executable:
|
15
|
+
dependencies:
|
16
|
+
- !ruby/object:Gem::Dependency
|
17
|
+
name: builder
|
18
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
19
|
+
none: false
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: "0"
|
24
|
+
type: :runtime
|
25
|
+
prerelease: false
|
26
|
+
version_requirements: *id001
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: nokogiri
|
29
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
30
|
+
none: false
|
31
|
+
requirements:
|
32
|
+
- - ">="
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: "0"
|
35
|
+
type: :runtime
|
36
|
+
prerelease: false
|
37
|
+
version_requirements: *id002
|
38
|
+
- !ruby/object:Gem::Dependency
|
39
|
+
name: zip
|
40
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ">="
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: "0"
|
46
|
+
type: :runtime
|
47
|
+
prerelease: false
|
48
|
+
version_requirements: *id003
|
49
|
+
- !ruby/object:Gem::Dependency
|
50
|
+
name: mocha
|
51
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
52
|
+
none: false
|
53
|
+
requirements:
|
54
|
+
- - ">="
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
version: "0"
|
57
|
+
type: :development
|
58
|
+
prerelease: false
|
59
|
+
version_requirements: *id004
|
60
|
+
- !ruby/object:Gem::Dependency
|
61
|
+
name: rake
|
62
|
+
requirement: &id005 !ruby/object:Gem::Requirement
|
63
|
+
none: false
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: "0"
|
68
|
+
type: :development
|
69
|
+
prerelease: false
|
70
|
+
version_requirements: *id005
|
71
|
+
- !ruby/object:Gem::Dependency
|
72
|
+
name: rspec
|
73
|
+
requirement: &id006 !ruby/object:Gem::Requirement
|
74
|
+
none: false
|
75
|
+
requirements:
|
76
|
+
- - ~>
|
77
|
+
- !ruby/object:Gem::Version
|
78
|
+
version: "2.0"
|
79
|
+
type: :development
|
80
|
+
prerelease: false
|
81
|
+
version_requirements: *id006
|
82
|
+
description: |-
|
83
|
+
Bindery is a Ruby library for easy packaging of ebooks.
|
84
|
+
You supply the chapter content (in HTML format) and explain the book's structure to bindery,
|
85
|
+
and bindery generates the various other files required by ebook formats and assembles them
|
86
|
+
into a completed book suitable for installation on an ebook reader.
|
87
|
+
email:
|
88
|
+
- glv@vanderburg.org
|
89
|
+
executables: []
|
90
|
+
|
91
|
+
extensions: []
|
92
|
+
|
93
|
+
extra_rdoc_files: []
|
94
|
+
|
95
|
+
files:
|
96
|
+
- .gitignore
|
97
|
+
- Gemfile
|
98
|
+
- README.md
|
99
|
+
- Rakefile
|
100
|
+
- TODO.taskpaper
|
101
|
+
- bindery.gemspec
|
102
|
+
- examples/ashtray/book.rb
|
103
|
+
- examples/trivial/book.rb
|
104
|
+
- examples/trivial/chapter_1.xhtml
|
105
|
+
- examples/trivial/chapter_2.xhtml
|
106
|
+
- lib/bindery.rb
|
107
|
+
- lib/bindery/bindery.rb
|
108
|
+
- lib/bindery/book.rb
|
109
|
+
- lib/bindery/book_builder.rb
|
110
|
+
- lib/bindery/chapter.rb
|
111
|
+
- lib/bindery/extensions/file.rb
|
112
|
+
- lib/bindery/extensions/string.rb
|
113
|
+
- lib/bindery/extensions/zip_file.rb
|
114
|
+
- lib/bindery/formats/epub.rb
|
115
|
+
- lib/bindery/version.rb
|
116
|
+
- spec/bindery/bindery_spec.rb
|
117
|
+
- spec/bindery/book_builder_spec.rb
|
118
|
+
- spec/spec_helper.rb
|
119
|
+
has_rdoc: true
|
120
|
+
homepage: http://github.com/glv/bindery
|
121
|
+
licenses: []
|
122
|
+
|
123
|
+
post_install_message:
|
124
|
+
rdoc_options: []
|
125
|
+
|
126
|
+
require_paths:
|
127
|
+
- lib
|
128
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
129
|
+
none: false
|
130
|
+
requirements:
|
131
|
+
- - ">="
|
132
|
+
- !ruby/object:Gem::Version
|
133
|
+
hash: -3490394669822348887
|
134
|
+
segments:
|
135
|
+
- 0
|
136
|
+
version: "0"
|
137
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
138
|
+
none: false
|
139
|
+
requirements:
|
140
|
+
- - ">="
|
141
|
+
- !ruby/object:Gem::Version
|
142
|
+
hash: -3490394669822348887
|
143
|
+
segments:
|
144
|
+
- 0
|
145
|
+
version: "0"
|
146
|
+
requirements: []
|
147
|
+
|
148
|
+
rubyforge_project: bindery
|
149
|
+
rubygems_version: 1.5.2
|
150
|
+
signing_key:
|
151
|
+
specification_version: 3
|
152
|
+
summary: Easy ebook packaging with Ruby
|
153
|
+
test_files:
|
154
|
+
- spec/bindery/bindery_spec.rb
|
155
|
+
- spec/bindery/book_builder_spec.rb
|
156
|
+
- spec/spec_helper.rb
|