bindery 0.0.3 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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 [![Build Status](https://secure.travis-ci.org/glv/bindery.png)](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
|