bindery 0.0.3 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +1 -0
- data/.travis.yml +4 -0
- data/ChangeLog +18 -0
- data/README.md +23 -12
- data/TODO.taskpaper +3 -2
- data/examples/ashtray/book.rb +2 -2
- data/examples/trivial/appendices.xhtml +1 -0
- data/examples/trivial/book.rb +18 -12
- data/examples/trivial/chapter_2.xhtml +3 -1
- data/examples/trivial/colophon.xhtml +3 -0
- data/examples/trivial/errata.xhtml +5 -0
- data/examples/trivial/index.xhtml +3 -0
- data/examples/vanier_monads/book.rb +52 -0
- data/lib/bindery.rb +2 -1
- data/lib/bindery/book.rb +16 -6
- data/lib/bindery/book_builder.rb +56 -55
- data/lib/bindery/content_methods.rb +36 -0
- data/lib/bindery/division.rb +43 -0
- data/lib/bindery/formats/epub.rb +78 -27
- data/lib/bindery/version.rb +1 -1
- data/spec/bindery/book_builder_spec.rb +23 -44
- metadata +111 -78
- data/lib/bindery/chapter.rb +0 -28
data/.gitignore
CHANGED
data/.travis.yml
ADDED
data/ChangeLog
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
2013-08-10 Glenn Vanderburg
|
2
|
+
|
3
|
+
* content_methods.rb: Completely changed the behavior of the
|
4
|
+
chapter method when a block is passed. The previous design was
|
5
|
+
embarrassingly bad. There was no need to use the passed block
|
6
|
+
to dynamically calculate the parameters that could be passed
|
7
|
+
directly to the method; Ruby has other ways of doing that. Now,
|
8
|
+
the block is used to define hierarchical book structure: nested
|
9
|
+
chapters, parts, sections, etc.
|
10
|
+
|
11
|
+
If you were using the block to dynamically build content and
|
12
|
+
return the chapter title and file name, simply move the content of
|
13
|
+
the block to a method, and then you can splice the returned title
|
14
|
+
and filename into the arguments of chapter using the splat
|
15
|
+
operator (*) or (if the block used the strategy of returning a
|
16
|
+
hash) include the activesupport gem, require
|
17
|
+
'active_support/core_ext/hash/slice', and use Hash#slice(:title,
|
18
|
+
:filename).
|
data/README.md
CHANGED
@@ -1,11 +1,11 @@
|
|
1
|
-
# Bindery
|
1
|
+
# Bindery [](http://travis-ci.org/glv/bindery)
|
2
2
|
|
3
3
|
[Bindery][] is a [Ruby][] library for packaging ebooks.
|
4
4
|
|
5
5
|
Electronic book formats are typically rather simple.
|
6
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
7
|
But there are numerous tricky details, and a lot of redundancy in the metadata.
|
8
|
-
Bindery aims to simplify the process.
|
8
|
+
Bindery aims to simplify the process, while stopping short of being a full ebook authoring system.
|
9
9
|
|
10
10
|
To use Bindery, you write a simple Ruby program that describes the book's structure and important metadata.
|
11
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.
|
@@ -49,13 +49,11 @@ Bindery.book do |b|
|
|
49
49
|
# is a Ruby Markdown library that supports numerous common extensions
|
50
50
|
# to the basic Markdown syntax.)
|
51
51
|
Dir['*.maruku'].sort.each do |file_name|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
{ :title => doc.attributes[:title], :file => output_file_name }
|
58
|
-
end
|
52
|
+
output_file_name = file_name.sub(/\.maruku$/, '.xhtml_gen')
|
53
|
+
doc = Maruku.new(IO.read(file_name))
|
54
|
+
open(output_file_name, "w") {|os| os.write(doc.to_html)}
|
55
|
+
|
56
|
+
b.chapter doc.attributes[:title], output_file_name
|
59
57
|
end
|
60
58
|
|
61
59
|
b.backmatter 'Colophon' # filename assumed to be colophon.xhtml
|
@@ -64,18 +62,22 @@ end
|
|
64
62
|
|
65
63
|
## Status
|
66
64
|
|
67
|
-
Bindery is currently limited to generating EPUB books
|
65
|
+
Bindery is currently limited to generating EPUB books.
|
68
66
|
|
69
67
|
It is also in a very early stage.
|
70
68
|
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
69
|
But the basics are there, and contributions are welcome.
|
73
70
|
|
71
|
+
Generated EPUB books will be valid according to [epubcheck][] *except* perhaps for chapter content.
|
72
|
+
EPUB places some additional restrictions on XHTML and CSS, and if the supplied chapter content violates those restrictions then the EPUB file will be invalid.
|
73
|
+
Most EPUB readers are fairly permissive about such things, but some are more particular.
|
74
|
+
I plan to build support for tidying up the XHTML and CSS and eliminating invalid constructs, but at the moment that's a low priority.
|
75
|
+
|
74
76
|
Planned features include:
|
75
77
|
|
76
78
|
* options for sections (with and without section title pages) rather than just a flat chapter structure.
|
77
79
|
* additional metadata including all of [Dublin Core][].
|
78
|
-
*
|
80
|
+
* title and cover pages
|
79
81
|
* support for multiple stylesheets
|
80
82
|
* support for generating Mobipocket books
|
81
83
|
|
@@ -102,6 +104,13 @@ Bindery is designed to support those use cases, not just brand new books written
|
|
102
104
|
If you want to publish in both electronic and paper formats, Bindery is probably not for you.
|
103
105
|
Bindery is targeted at electronic books only.
|
104
106
|
|
107
|
+
## Acknowledgments
|
108
|
+
|
109
|
+
In February 2013, about a year and a half after initially writing Bindery,
|
110
|
+
I discovered the [Python Epub Builder][pyepub], written by (apparently)
|
111
|
+
Bin Tan. It was more complete than Bindery was at the time, and it inspired
|
112
|
+
me to work on Bindery again, and gave me some valuable ideas.
|
113
|
+
|
105
114
|
[adobe indesign]: http://www.adobe.com/products/indesign.html
|
106
115
|
[asciidoc]: http://www.methods.co.nz/asciidoc/
|
107
116
|
[best software writing]: http://www.apress.com/9781590595008
|
@@ -109,6 +118,7 @@ Bindery is targeted at electronic books only.
|
|
109
118
|
[devonthink]: http://www.devon-technologies.com/products/devonthink/index.html
|
110
119
|
[dublin core]: http://dublincore.org/sm
|
111
120
|
[epub]: http://idpf.org/epub
|
121
|
+
[epubcheck]: http://code.google.com/p/epubcheck/
|
112
122
|
[git]: http://git-scm.com/
|
113
123
|
[github]: http://github.com/
|
114
124
|
[git-scribe]: http://github.com/schacon/git-scribe
|
@@ -118,6 +128,7 @@ Bindery is targeted at electronic books only.
|
|
118
128
|
[johnson dt2]: http://boingboing.net/2009/01/27/diy-how-to-write-a-b.html
|
119
129
|
[markdown]: http://daringfireball.net/projects/markdown/
|
120
130
|
[microsoft word]: http://office.microsoft.com/word/
|
131
|
+
[pyepub]: http://code.google.com/p/python-epub-builder/
|
121
132
|
[rake]: http://rake.rubyforge.org/
|
122
133
|
[ruby]: http://ruby-lang.org/
|
123
134
|
[sproutcore]: http://www.sproutcore.com/
|
data/TODO.taskpaper
CHANGED
@@ -1,13 +1,14 @@
|
|
1
1
|
- Flesh out validation
|
2
2
|
Consider moving details to format modules.
|
3
3
|
Investigate Mobi validity constraints to see whether that makes sense.
|
4
|
+
- Cache fetched image (and other) files so we don't have to fetch them twice while building multiple formats.
|
4
5
|
- Add more metadata support
|
5
6
|
- subtitle
|
6
7
|
- different kinds of authors
|
7
8
|
- Title page
|
8
9
|
- Title image
|
9
|
-
- Image support
|
10
|
-
- A basic CSS file
|
10
|
+
- Image support @done
|
11
|
+
- A basic CSS file @done
|
11
12
|
- Support for alternative CSS files
|
12
13
|
- Frontmatter
|
13
14
|
- Nested chapter structures
|
data/examples/ashtray/book.rb
CHANGED
@@ -40,7 +40,7 @@ def process_part(url, i)
|
|
40
40
|
content.children[0..1].each{|c| c.remove}
|
41
41
|
os.write content.serialize
|
42
42
|
end
|
43
|
-
|
43
|
+
[title, file_name]
|
44
44
|
end
|
45
45
|
|
46
46
|
Bindery.book do |b|
|
@@ -53,6 +53,6 @@ Bindery.book do |b|
|
|
53
53
|
b.language 'en'
|
54
54
|
|
55
55
|
PARTS.each_with_index do |url, i|
|
56
|
-
b.chapter
|
56
|
+
b.chapter *process_part(url, i)
|
57
57
|
end
|
58
58
|
end
|
@@ -0,0 +1 @@
|
|
1
|
+
<h2>Appendices</h2>
|
data/examples/trivial/book.rb
CHANGED
@@ -11,17 +11,23 @@ Bindery.book do |b|
|
|
11
11
|
|
12
12
|
b.chapter "Chapter 1", 'chapter_1.xhtml'
|
13
13
|
b.chapter "Chapter 2", 'chapter_2.xhtml', :body_only => false
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
14
|
+
|
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
|
+
fn = 'chapter_3.xhtml_gen'
|
19
|
+
File.open(fn, '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
|
+
b.chapter "Chapter 3", fn
|
26
|
+
|
27
|
+
b.part("Appendices", 'appendices.xhtml') do |div|
|
28
|
+
div.appendix "Appendix 1: Errata", 'errata.xhtml'
|
29
|
+
div.appendix "Appendix 2: Index", 'index.xhtml'
|
26
30
|
end
|
31
|
+
|
32
|
+
b.part "Colophon", 'colophon.xhtml'
|
27
33
|
end
|
@@ -9,6 +9,8 @@
|
|
9
9
|
<body>
|
10
10
|
<h2>Chapter 2</h2>
|
11
11
|
|
12
|
-
<p>Unlike Chapter 1, this chapter's source file is a full, valid XHTML
|
12
|
+
<p>Unlike Chapter 1, this chapter's source file is a full, valid XHTML
|
13
|
+
file, not just the body. It will cause some errors for an epub
|
14
|
+
reader that really validates the spec.</p>
|
13
15
|
</body>
|
14
16
|
</html>
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'bindery'
|
2
|
+
require 'open-uri'
|
3
|
+
require 'nokogiri'
|
4
|
+
#require 'tidy_ffi'
|
5
|
+
|
6
|
+
PARTS = [
|
7
|
+
'http://webcache.googleusercontent.com/search?q=cache:http://mvanier.livejournal.com/3917.html',
|
8
|
+
'http://webcache.googleusercontent.com/search?q=cache:http://mvanier.livejournal.com/4305.html',
|
9
|
+
'http://webcache.googleusercontent.com/search?q=cache:http://mvanier.livejournal.com/4586.html',
|
10
|
+
'http://webcache.googleusercontent.com/search?q=cache:http://mvanier.livejournal.com/4647.html',
|
11
|
+
'http://webcache.googleusercontent.com/search?q=cache:http://mvanier.livejournal.com/5103.html',
|
12
|
+
'http://webcache.googleusercontent.com/search?q=cache:http://mvanier.livejournal.com/5343.html',
|
13
|
+
'http://webcache.googleusercontent.com/search?q=cache:http://mvanier.livejournal.com/5406.html',
|
14
|
+
'http://webcache.googleusercontent.com/search?q=cache:http://mvanier.livejournal.com/5846.html'
|
15
|
+
]
|
16
|
+
|
17
|
+
TITLES = [
|
18
|
+
"Part 1: Basics",
|
19
|
+
nil,
|
20
|
+
nil,
|
21
|
+
nil,
|
22
|
+
"Part 5: Error-Handling Monads",
|
23
|
+
"Part 6: More on Error-Handling Monads",
|
24
|
+
"Part 7: State Monads",
|
25
|
+
"Part 8: More on State Monads"
|
26
|
+
]
|
27
|
+
|
28
|
+
def process_part(url, i)
|
29
|
+
file_name = "chapter_#{i+1}.xhtml_gen"
|
30
|
+
doc = Nokogiri::HTML(open(url), nil, 'UTF-8')
|
31
|
+
title = TITLES[i] || doc.at_css('h1.b-singlepost-title').text.sub(/^.*?Tutorial \(p(art .*?)\).*$/, 'P\\1')
|
32
|
+
open(file_name, 'w') do |os|
|
33
|
+
content = doc.at_css('div.b-singlepost-body')
|
34
|
+
os.puts "<h1>#{title}</h1>"
|
35
|
+
os.write content.serialize
|
36
|
+
end
|
37
|
+
[title, file_name]
|
38
|
+
end
|
39
|
+
|
40
|
+
Bindery.book do |b|
|
41
|
+
b.output 'mvanier_monad_tutorial'
|
42
|
+
b.format :epub
|
43
|
+
|
44
|
+
b.title "Yet Another Monad Tutorial"
|
45
|
+
b.author 'Mike Vanier'
|
46
|
+
b.url 'http://glenn.mp/book/mvanier_monad_tutorial'
|
47
|
+
b.language 'en'
|
48
|
+
|
49
|
+
PARTS.each_with_index do |url, i|
|
50
|
+
b.chapter *process_part(url, i)
|
51
|
+
end
|
52
|
+
end
|
data/lib/bindery.rb
CHANGED
@@ -4,7 +4,8 @@ require 'bindery/extensions/file'
|
|
4
4
|
require 'bindery/version'
|
5
5
|
require 'bindery/bindery'
|
6
6
|
require 'bindery/book'
|
7
|
+
require 'bindery/content_methods'
|
7
8
|
require 'bindery/book_builder'
|
8
|
-
require 'bindery/
|
9
|
+
require 'bindery/division'
|
9
10
|
|
10
11
|
require 'bindery/formats/epub'
|
data/lib/bindery/book.rb
CHANGED
@@ -1,13 +1,23 @@
|
|
1
1
|
module Bindery
|
2
2
|
class Book
|
3
|
+
class Metadata < Struct.new(:name, :value, :options)
|
4
|
+
end
|
5
|
+
|
6
|
+
class DublinMetadata < Metadata
|
7
|
+
end
|
8
|
+
|
3
9
|
attr_accessor :output, :url, :isbn, :title, :language, :author, :subtitle
|
4
10
|
|
11
|
+
def metadata
|
12
|
+
@metadata ||= []
|
13
|
+
end
|
14
|
+
|
5
15
|
def formats
|
6
16
|
@formats ||= []
|
7
17
|
end
|
8
18
|
|
9
|
-
def
|
10
|
-
@
|
19
|
+
def divisions
|
20
|
+
@divisions ||= []
|
11
21
|
end
|
12
22
|
|
13
23
|
def full_title
|
@@ -15,14 +25,14 @@ module Bindery
|
|
15
25
|
end
|
16
26
|
|
17
27
|
def valid?
|
18
|
-
configuration_valid? && metadata_valid? &&
|
28
|
+
configuration_valid? && metadata_valid? && divisions_valid?
|
19
29
|
end
|
20
30
|
|
21
31
|
def configuration_valid?
|
22
32
|
true
|
23
33
|
# formats specified or correctly defaulted
|
24
34
|
# ouput specified
|
25
|
-
# at least one
|
35
|
+
# at least one division
|
26
36
|
end
|
27
37
|
|
28
38
|
def metadata_valid?
|
@@ -31,8 +41,8 @@ module Bindery
|
|
31
41
|
# what is there is correct
|
32
42
|
end
|
33
43
|
|
34
|
-
def
|
35
|
-
|
44
|
+
def divisions_valid?
|
45
|
+
divisions.all?{|div| div.valid?} # && division file names are unique
|
36
46
|
end
|
37
47
|
|
38
48
|
def generate
|
data/lib/bindery/book_builder.rb
CHANGED
@@ -3,6 +3,27 @@ module Bindery
|
|
3
3
|
class BookBuilder
|
4
4
|
attr_accessor :book
|
5
5
|
|
6
|
+
Metadata = {
|
7
|
+
:contributor => nil,
|
8
|
+
:cover => :special,
|
9
|
+
:coverage => nil,
|
10
|
+
:creator => nil,
|
11
|
+
:date => nil,
|
12
|
+
:description => nil,
|
13
|
+
:format => nil,
|
14
|
+
:identifier => :required,
|
15
|
+
:language => :required,
|
16
|
+
:publisher => nil,
|
17
|
+
:relation => nil,
|
18
|
+
:rights => nil,
|
19
|
+
:source => nil,
|
20
|
+
:subject => nil,
|
21
|
+
:title => :required,
|
22
|
+
:type => nil
|
23
|
+
}
|
24
|
+
|
25
|
+
include ContentMethods
|
26
|
+
|
6
27
|
def initialize
|
7
28
|
self.book = ::Bindery::Book.new
|
8
29
|
end
|
@@ -16,7 +37,42 @@ module Bindery
|
|
16
37
|
raise "output already set to #{book.output}" unless book.output.nil?
|
17
38
|
book.output = basename.to_s
|
18
39
|
end
|
40
|
+
|
41
|
+
def divisions
|
42
|
+
book.divisions
|
43
|
+
end
|
44
|
+
|
45
|
+
# ----------------------------------------------------
|
46
|
+
# Metadata elements
|
47
|
+
|
48
|
+
# Allows grouping metadata elements together in a named block
|
49
|
+
# within the book specification. Use of this method is not necessary;
|
50
|
+
# all of the metadata methods can be called directly on the BookBuilder
|
51
|
+
# instance. It is usually best, though, to have them clearly grouped
|
52
|
+
# within a metadata block.
|
53
|
+
def metadata
|
54
|
+
yield self
|
55
|
+
end
|
19
56
|
|
57
|
+
def metadata_element(name, value, options={})
|
58
|
+
name_sym = name.to_sym
|
59
|
+
if Metadata[name_sym] == :special
|
60
|
+
book.metadata << Bindery::Book::Metadata.new(name_sym, value, options)
|
61
|
+
else
|
62
|
+
book.metadata << Bindery::Book::DublinMetadata.new(name_sym, value, options)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def method_missing(name, *args, &block)
|
67
|
+
if Metadata.include?(name.to_sym)
|
68
|
+
metadata_element(name, *args, &block)
|
69
|
+
else
|
70
|
+
super
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# TODO: Most of these could be switched to be general metadata
|
75
|
+
# objects. Should they be?
|
20
76
|
def url(url)
|
21
77
|
book.url = url
|
22
78
|
end
|
@@ -41,61 +97,6 @@ module Bindery
|
|
41
97
|
book.author = author
|
42
98
|
end
|
43
99
|
|
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
100
|
end
|
100
101
|
|
101
102
|
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Bindery
|
2
|
+
module ContentMethods
|
3
|
+
|
4
|
+
# Add a division to the book.
|
5
|
+
#
|
6
|
+
# The following options are supported:
|
7
|
+
# [:body_only] the file contains only the body of the XHTML document,
|
8
|
+
# and Bindery should wrap it to create a valid document.
|
9
|
+
# Defaults to true.
|
10
|
+
def div(div_type, title, filename, options={})
|
11
|
+
options = {:body_only => true}.merge(options)
|
12
|
+
raise ArgumentError, "title not specified" if title.nil?
|
13
|
+
raise ArgumentError, "file not specified" if filename.nil?
|
14
|
+
div = Division.new(div_type, title, filename, options)
|
15
|
+
divisions << div
|
16
|
+
yield div if block_given?
|
17
|
+
end
|
18
|
+
|
19
|
+
def chapter(title, filename, options={}, &block)
|
20
|
+
div('chapter', title, filename, options, &block)
|
21
|
+
end
|
22
|
+
|
23
|
+
def section(title, filename, options={}, &block)
|
24
|
+
div('section', title, filename, options, &block)
|
25
|
+
end
|
26
|
+
|
27
|
+
def part(title, filename, options={}, &block)
|
28
|
+
div('part', title, filename, options, &block)
|
29
|
+
end
|
30
|
+
|
31
|
+
def appendix(title, filename, options={}, &block)
|
32
|
+
div('appendix', title, filename, options, &block)
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Bindery
|
2
|
+
class Division
|
3
|
+
attr_accessor :div_type
|
4
|
+
attr_accessor :file
|
5
|
+
attr_accessor :title
|
6
|
+
attr_accessor :options
|
7
|
+
|
8
|
+
include ContentMethods
|
9
|
+
|
10
|
+
def initialize(div_type, title, file, options)
|
11
|
+
self.div_type = div_type
|
12
|
+
self.title = title
|
13
|
+
self.file = file
|
14
|
+
self.options = options
|
15
|
+
end
|
16
|
+
|
17
|
+
def valid?
|
18
|
+
true
|
19
|
+
# ??? title specified? Is this required by the spec? Think about
|
20
|
+
# in what sense a chapter needs a title. Wouldn't it be nice
|
21
|
+
# if (say) Pratchett's books could be broken up a bit even
|
22
|
+
# though he doesn't have titled chapters? Aren't there books
|
23
|
+
# with actual chapters but no titles? Does epub provide
|
24
|
+
# another way to provide subdivisions without separate
|
25
|
+
# chapters that would break up the text flow?
|
26
|
+
# file exists, readable
|
27
|
+
# file content properly formed? Does that matter? Can we
|
28
|
+
# verify it?
|
29
|
+
end
|
30
|
+
|
31
|
+
def divisions
|
32
|
+
@divisions ||= []
|
33
|
+
end
|
34
|
+
|
35
|
+
def body_only?
|
36
|
+
options.fetch(:body_only, true)
|
37
|
+
end
|
38
|
+
|
39
|
+
def include_images?
|
40
|
+
options.fetch(:include_images, true)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
data/lib/bindery/formats/epub.rb
CHANGED
@@ -35,7 +35,7 @@ module Bindery
|
|
35
35
|
def initialize(book)
|
36
36
|
self.book = book
|
37
37
|
book.extend BookMethods
|
38
|
-
book.
|
38
|
+
book.divisions.each{|division| division.extend DivisionMethods}
|
39
39
|
self.manifest_entries = []
|
40
40
|
end
|
41
41
|
|
@@ -48,8 +48,8 @@ module Bindery
|
|
48
48
|
zipfile.write_file 'META-INF/container.xml', container
|
49
49
|
|
50
50
|
# also frontmatter, backmatter
|
51
|
-
book.
|
52
|
-
|
51
|
+
book.divisions.each do |division|
|
52
|
+
write_division(division, zipfile)
|
53
53
|
end
|
54
54
|
|
55
55
|
zipfile.mkdir 'css'
|
@@ -93,7 +93,7 @@ module Bindery
|
|
93
93
|
}
|
94
94
|
|
95
95
|
xm.manifest {
|
96
|
-
book.
|
96
|
+
book.divisions.each{|division| division.write_item(xm)}
|
97
97
|
# also frontmatter, backmatter
|
98
98
|
xm.item 'id'=>'stylesheet', 'href'=>'css/book.css', 'media-type'=>'text/css'
|
99
99
|
manifest_entries.each do |entry|
|
@@ -104,7 +104,7 @@ module Bindery
|
|
104
104
|
}
|
105
105
|
|
106
106
|
xm.spine('toc'=>'ncx') {
|
107
|
-
book.
|
107
|
+
book.divisions.each{|division| division.write_itemref(xm)}
|
108
108
|
}
|
109
109
|
|
110
110
|
# xm.guide {
|
@@ -134,36 +134,31 @@ module Bindery
|
|
134
134
|
}
|
135
135
|
|
136
136
|
xm.navMap {
|
137
|
-
play_order =
|
137
|
+
play_order = 0
|
138
138
|
|
139
139
|
# also frontmatter, backmatter
|
140
|
-
book.
|
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
|
-
}
|
140
|
+
book.divisions.each do |division|
|
147
141
|
play_order += 1
|
142
|
+
play_order = division.write_navpoint(xm, play_order)
|
148
143
|
end
|
149
144
|
}
|
150
145
|
}
|
151
146
|
end
|
152
147
|
|
153
|
-
def
|
148
|
+
def write_division(division, zipfile)
|
154
149
|
save_options = Nokogiri::XML::Node::SaveOptions
|
155
|
-
File.open(
|
150
|
+
File.open(division.file, 'r:UTF-8') do |ch_in|
|
156
151
|
doc = Nokogiri.HTML(ch_in.read)
|
157
|
-
include_images(doc, zipfile) if
|
158
|
-
zipfile.get_output_stream(
|
159
|
-
if
|
160
|
-
# FIXME: must HTML-escape the
|
152
|
+
include_images(doc, zipfile) if division.include_images?
|
153
|
+
zipfile.get_output_stream(division.epub_output_file) do |ch_out|
|
154
|
+
if division.body_only?
|
155
|
+
# FIXME: must HTML-escape the division title
|
161
156
|
ch_out.write %{|<?xml version="1.0" encoding="UTF-8" ?>
|
162
157
|
|<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
|
163
158
|
|<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
|
164
159
|
|<head>
|
165
160
|
| <meta http-equiv="Content-Type" content="application/xhtml+xml; charset=utf-8" />
|
166
|
-
| <title>#{
|
161
|
+
| <title>#{division.title}</title>
|
167
162
|
| <link rel="stylesheet" href="css/book.css" type="text/css" />
|
168
163
|
|</head>
|
169
164
|
|}.strip_margin
|
@@ -175,6 +170,9 @@ module Bindery
|
|
175
170
|
end
|
176
171
|
end
|
177
172
|
end
|
173
|
+
division.divisions.each do |div|
|
174
|
+
write_division(div, zipfile)
|
175
|
+
end
|
178
176
|
end
|
179
177
|
|
180
178
|
def include_images(doc, zipfile)
|
@@ -184,13 +182,17 @@ module Bindery
|
|
184
182
|
url = img['src']
|
185
183
|
img_fn = make_image_file_name(zipfile, url)
|
186
184
|
# TODO: These images should be cached somewhere for multi-format runs
|
187
|
-
|
188
|
-
|
189
|
-
|
185
|
+
begin
|
186
|
+
open(url, 'r') do |is|
|
187
|
+
zipfile.get_output_stream(img_fn) do |os|
|
188
|
+
os.write is.read
|
189
|
+
end
|
190
190
|
end
|
191
|
+
add_manifest_entry(img_fn)
|
192
|
+
img['src'] = img_fn
|
193
|
+
rescue OpenURI::HTTPError => ex
|
194
|
+
puts "Image fetch failed: #{ex.message} (#{url})"
|
191
195
|
end
|
192
|
-
add_manifest_entry(img_fn)
|
193
|
-
img['src'] = img_fn
|
194
196
|
end
|
195
197
|
end
|
196
198
|
|
@@ -291,7 +293,7 @@ module Bindery
|
|
291
293
|
end
|
292
294
|
|
293
295
|
def depth
|
294
|
-
|
296
|
+
(divisions.map(&:depth) + [0]).max
|
295
297
|
end
|
296
298
|
|
297
299
|
def ident
|
@@ -299,7 +301,12 @@ module Bindery
|
|
299
301
|
end
|
300
302
|
end
|
301
303
|
|
302
|
-
module
|
304
|
+
module DivisionMethods
|
305
|
+
|
306
|
+
def self.extended(obj)
|
307
|
+
obj.divisions.each{|division| division.extend DivisionMethods}
|
308
|
+
end
|
309
|
+
|
303
310
|
def epub_id
|
304
311
|
@epub_id ||= File.stemname(file)
|
305
312
|
end
|
@@ -307,6 +314,50 @@ module Bindery
|
|
307
314
|
def epub_output_file
|
308
315
|
@epub_output_file ||= "#{epub_id}.xhtml"
|
309
316
|
end
|
317
|
+
|
318
|
+
def depth
|
319
|
+
(divisions.map(&:depth) + [0]).max + 1
|
320
|
+
end
|
321
|
+
|
322
|
+
def write_item(xm)
|
323
|
+
xm.item('id' => epub_id,
|
324
|
+
'href' => epub_output_file,
|
325
|
+
'media-type' => 'application/xhtml+xml')
|
326
|
+
divisions.each{|div| div.write_item(xm)}
|
327
|
+
end
|
328
|
+
|
329
|
+
def write_itemref(xm)
|
330
|
+
xm.itemref('idref' => epub_id)
|
331
|
+
divisions.each{|div| div.write_itemref(xm)}
|
332
|
+
end
|
333
|
+
|
334
|
+
def write_navpoint(xm, play_order)
|
335
|
+
xm.navPoint('class'=>'chapter', 'id'=>epub_id, 'playOrder'=>play_order) {
|
336
|
+
xm.navLabel {
|
337
|
+
xm.text title
|
338
|
+
}
|
339
|
+
xm.content 'src'=>epub_output_file
|
340
|
+
divisions.each do |division|
|
341
|
+
play_order += 1
|
342
|
+
play_order = division.write_navpoint(xm, play_order)
|
343
|
+
end
|
344
|
+
}
|
345
|
+
play_order
|
346
|
+
end
|
347
|
+
|
348
|
+
end
|
349
|
+
|
350
|
+
module MetadataMethods
|
351
|
+
def to_xml(builder)
|
352
|
+
builder.meta(options.merge(:name => name, :content => value))
|
353
|
+
%{<dc:#{name}>#{value}</dc:#{name}>}
|
354
|
+
end
|
355
|
+
end
|
356
|
+
|
357
|
+
module DublinMetadataMethods
|
358
|
+
def to_xml(builder)
|
359
|
+
builder.dc name, value, options
|
360
|
+
end
|
310
361
|
end
|
311
362
|
end
|
312
363
|
end
|
data/lib/bindery/version.rb
CHANGED
@@ -38,53 +38,32 @@ describe Bindery::BookBuilder do
|
|
38
38
|
end
|
39
39
|
end
|
40
40
|
|
41
|
-
describe "#
|
41
|
+
describe "#div" do
|
42
42
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
43
|
+
it "raises an exception if title or filename are not supplied" do
|
44
|
+
expect { subject.div("chapter", nil, "fn") }.to raise_error(ArgumentError, "title not specified")
|
45
|
+
expect { subject.div("chapter", "A", nil) }.to raise_error(ArgumentError, "file not specified")
|
46
|
+
end
|
47
|
+
|
48
|
+
it "stores a new division with the supplied options" do
|
49
|
+
subject.div 'excerpt', "Foo", "bar.xhtml", :a => :b
|
50
|
+
book.divisions.should have(1).elements
|
51
|
+
div = book.divisions.first
|
52
|
+
div.div_type.should == 'excerpt'
|
53
|
+
div.title.should == "Foo"
|
54
|
+
div.file.should == "bar.xhtml"
|
55
|
+
div.options.should include(:a => :b)
|
55
56
|
end
|
56
57
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
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
|
58
|
+
end
|
59
|
+
|
60
|
+
%w[chapter section part appendix].each do |div_type|
|
61
|
+
describe "##{div_type}" do
|
62
|
+
it "calls #div with the appropriate type" do
|
63
|
+
subject.expects(div_type.to_sym).once
|
64
|
+
subject.send(div_type, :title, :filename, {:a => :b})
|
85
65
|
end
|
86
|
-
|
87
66
|
end
|
88
|
-
|
89
67
|
end
|
90
|
-
|
68
|
+
|
69
|
+
end
|
metadata
CHANGED
@@ -1,113 +1,150 @@
|
|
1
|
-
--- !ruby/object:Gem::Specification
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
2
|
name: bindery
|
3
|
-
version: !ruby/object:Gem::Version
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 2.0.0
|
4
5
|
prerelease:
|
5
|
-
version: 0.0.3
|
6
6
|
platform: ruby
|
7
|
-
authors:
|
7
|
+
authors:
|
8
8
|
- Glenn Vanderburg
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
dependencies:
|
16
|
-
- !ruby/object:Gem::Dependency
|
12
|
+
date: 2014-12-16 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
17
15
|
name: builder
|
18
|
-
requirement:
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
19
17
|
none: false
|
20
|
-
requirements:
|
21
|
-
- -
|
22
|
-
- !ruby/object:Gem::Version
|
23
|
-
version:
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
24
22
|
type: :runtime
|
25
23
|
prerelease: false
|
26
|
-
version_requirements:
|
27
|
-
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
28
31
|
name: nokogiri
|
29
|
-
requirement:
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
30
33
|
none: false
|
31
|
-
requirements:
|
32
|
-
- -
|
33
|
-
- !ruby/object:Gem::Version
|
34
|
-
version:
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
35
38
|
type: :runtime
|
36
39
|
prerelease: false
|
37
|
-
version_requirements:
|
38
|
-
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
39
47
|
name: zip
|
40
|
-
requirement:
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
41
49
|
none: false
|
42
|
-
requirements:
|
43
|
-
- -
|
44
|
-
- !ruby/object:Gem::Version
|
45
|
-
version:
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
46
54
|
type: :runtime
|
47
55
|
prerelease: false
|
48
|
-
version_requirements:
|
49
|
-
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
- !ruby/object:Gem::Dependency
|
50
63
|
name: mocha
|
51
|
-
requirement:
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
52
65
|
none: false
|
53
|
-
requirements:
|
54
|
-
- -
|
55
|
-
- !ruby/object:Gem::Version
|
56
|
-
version:
|
66
|
+
requirements:
|
67
|
+
- - ! '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
57
70
|
type: :development
|
58
71
|
prerelease: false
|
59
|
-
version_requirements:
|
60
|
-
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
- !ruby/object:Gem::Dependency
|
61
79
|
name: rake
|
62
|
-
requirement:
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
63
81
|
none: false
|
64
|
-
requirements:
|
65
|
-
- -
|
66
|
-
- !ruby/object:Gem::Version
|
67
|
-
version:
|
82
|
+
requirements:
|
83
|
+
- - ! '>='
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '0'
|
68
86
|
type: :development
|
69
87
|
prerelease: false
|
70
|
-
version_requirements:
|
71
|
-
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ! '>='
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '0'
|
94
|
+
- !ruby/object:Gem::Dependency
|
72
95
|
name: rspec
|
73
|
-
requirement:
|
96
|
+
requirement: !ruby/object:Gem::Requirement
|
74
97
|
none: false
|
75
|
-
requirements:
|
98
|
+
requirements:
|
76
99
|
- - ~>
|
77
|
-
- !ruby/object:Gem::Version
|
78
|
-
version:
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '2.0'
|
79
102
|
type: :development
|
80
103
|
prerelease: false
|
81
|
-
version_requirements:
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
104
|
+
version_requirements: !ruby/object:Gem::Requirement
|
105
|
+
none: false
|
106
|
+
requirements:
|
107
|
+
- - ~>
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '2.0'
|
110
|
+
description: ! 'Bindery is a Ruby library for easy packaging of ebooks.
|
111
|
+
|
112
|
+
You supply the chapter content (in HTML format) and explain the book''s structure
|
113
|
+
to bindery,
|
114
|
+
|
115
|
+
and bindery generates the various other files required by ebook formats and assembles
|
116
|
+
them
|
117
|
+
|
118
|
+
into a completed book suitable for installation on an ebook reader.'
|
119
|
+
email:
|
88
120
|
- glv@vanderburg.org
|
89
121
|
executables: []
|
90
|
-
|
91
122
|
extensions: []
|
92
|
-
|
93
123
|
extra_rdoc_files: []
|
94
|
-
|
95
|
-
files:
|
124
|
+
files:
|
96
125
|
- .gitignore
|
126
|
+
- .travis.yml
|
127
|
+
- ChangeLog
|
97
128
|
- Gemfile
|
98
129
|
- README.md
|
99
130
|
- Rakefile
|
100
131
|
- TODO.taskpaper
|
101
132
|
- bindery.gemspec
|
102
133
|
- examples/ashtray/book.rb
|
134
|
+
- examples/trivial/appendices.xhtml
|
103
135
|
- examples/trivial/book.rb
|
104
136
|
- examples/trivial/chapter_1.xhtml
|
105
137
|
- examples/trivial/chapter_2.xhtml
|
138
|
+
- examples/trivial/colophon.xhtml
|
139
|
+
- examples/trivial/errata.xhtml
|
140
|
+
- examples/trivial/index.xhtml
|
141
|
+
- examples/vanier_monads/book.rb
|
106
142
|
- lib/bindery.rb
|
107
143
|
- lib/bindery/bindery.rb
|
108
144
|
- lib/bindery/book.rb
|
109
145
|
- lib/bindery/book_builder.rb
|
110
|
-
- lib/bindery/
|
146
|
+
- lib/bindery/content_methods.rb
|
147
|
+
- lib/bindery/division.rb
|
111
148
|
- lib/bindery/extensions/file.rb
|
112
149
|
- lib/bindery/extensions/string.rb
|
113
150
|
- lib/bindery/extensions/zip_file.rb
|
@@ -116,38 +153,34 @@ files:
|
|
116
153
|
- spec/bindery/bindery_spec.rb
|
117
154
|
- spec/bindery/book_builder_spec.rb
|
118
155
|
- spec/spec_helper.rb
|
119
|
-
has_rdoc: true
|
120
156
|
homepage: http://github.com/glv/bindery
|
121
157
|
licenses: []
|
122
|
-
|
123
158
|
post_install_message:
|
124
159
|
rdoc_options: []
|
125
|
-
|
126
|
-
require_paths:
|
160
|
+
require_paths:
|
127
161
|
- lib
|
128
|
-
required_ruby_version: !ruby/object:Gem::Requirement
|
162
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
129
163
|
none: false
|
130
|
-
requirements:
|
131
|
-
- -
|
132
|
-
- !ruby/object:Gem::Version
|
164
|
+
requirements:
|
165
|
+
- - ! '>='
|
166
|
+
- !ruby/object:Gem::Version
|
133
167
|
version: 1.9.2
|
134
|
-
required_rubygems_version: !ruby/object:Gem::Requirement
|
168
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
135
169
|
none: false
|
136
|
-
requirements:
|
137
|
-
- -
|
138
|
-
- !ruby/object:Gem::Version
|
139
|
-
|
140
|
-
segments:
|
170
|
+
requirements:
|
171
|
+
- - ! '>='
|
172
|
+
- !ruby/object:Gem::Version
|
173
|
+
version: '0'
|
174
|
+
segments:
|
141
175
|
- 0
|
142
|
-
|
176
|
+
hash: -394401607600380017
|
143
177
|
requirements: []
|
144
|
-
|
145
178
|
rubyforge_project: bindery
|
146
|
-
rubygems_version: 1.
|
179
|
+
rubygems_version: 1.8.23
|
147
180
|
signing_key:
|
148
181
|
specification_version: 3
|
149
182
|
summary: Easy ebook packaging with Ruby
|
150
|
-
test_files:
|
183
|
+
test_files:
|
151
184
|
- spec/bindery/bindery_spec.rb
|
152
185
|
- spec/bindery/book_builder_spec.rb
|
153
186
|
- spec/spec_helper.rb
|
data/lib/bindery/chapter.rb
DELETED
@@ -1,28 +0,0 @@
|
|
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
|