epub-maker 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +19 -0
- data/CHANGELOG.markdown +4 -0
- data/Gemfile +7 -0
- data/LICENSE.txt +22 -0
- data/README.markdown +154 -0
- data/Rakefile +11 -0
- data/bin/epub-maker +3 -0
- data/epub-maker.gemspec +41 -0
- data/lib/epub/maker.rb +113 -0
- data/lib/epub/maker/content_document.rb +36 -0
- data/lib/epub/maker/ocf.rb +60 -0
- data/lib/epub/maker/publication.rb +340 -0
- data/lib/epub/maker/task.rb +134 -0
- data/lib/epub/maker/version.rb +5 -0
- data/test/fixtures/book/META-INF/container.xml +6 -0
- data/test/fixtures/book/OPS/impl.xhtml +9 -0
- data/test/fixtures/book/OPS/item-1.xhtml +8 -0
- data/test/fixtures/book/OPS/item-2.xhtml +12 -0
- data/test/fixtures/book/OPS/nav.xhtml +26 -0
- data/test/fixtures/book/OPS/slideshow.xml +0 -0
- data/test/fixtures/book/OPS//343/203/253/343/203/274/343/203/210/343/203/225/343/202/241/343/202/244/343/203/253.opf +91 -0
- data/test/helper.rb +17 -0
- data/test/make_task.rake +24 -0
- data/test/schemas/epub-nav-30.rnc +10 -0
- data/test/schemas/epub-nav-30.sch +72 -0
- data/test/schemas/epub-xhtml-30.sch +377 -0
- data/test/schemas/ocf-container-30.rnc +16 -0
- data/test/test_inplace_editing.rb +62 -0
- data/test/test_maker.rb +66 -0
- data/test/test_maker_ocf.rb +61 -0
- data/test/test_maker_publication.rb +40 -0
- data/test/test_task.rb +56 -0
- metadata +333 -0
@@ -0,0 +1,340 @@
|
|
1
|
+
require 'epub/publication/package'
|
2
|
+
|
3
|
+
module EPUB
|
4
|
+
module Publication
|
5
|
+
class Package
|
6
|
+
def to_xml(options={:encoding => 'UTF-8'})
|
7
|
+
Nokogiri::XML::Builder.new(options) {|xml|
|
8
|
+
attrs = {
|
9
|
+
'version' => '3.0',
|
10
|
+
'xmlns' => EPUB::NAMESPACES['opf'],
|
11
|
+
'unique-identifier' => unique_identifier.id
|
12
|
+
}
|
13
|
+
[
|
14
|
+
['dir', dir],
|
15
|
+
['id', id],
|
16
|
+
['xml:lang', xml_lang],
|
17
|
+
['prefix', prefix.reduce('') {|attr, (pfx, iri)| [attr, [pfx, iri].join(':')].join(' ')}]
|
18
|
+
].each do |(name, value)|
|
19
|
+
next if value.nil? or value.empty?
|
20
|
+
attrs[name] = value
|
21
|
+
end
|
22
|
+
xml.package_(attrs) do
|
23
|
+
(EPUB::Publication::Package::CONTENT_MODELS - [:bindings, :guide]).each do |model|
|
24
|
+
__send__(model).to_xml_fragment xml
|
25
|
+
end
|
26
|
+
if bindings and !bindings.media_types.empty?
|
27
|
+
bindings.to_xml_fragment xml
|
28
|
+
end
|
29
|
+
end
|
30
|
+
}.to_xml
|
31
|
+
end
|
32
|
+
|
33
|
+
def make
|
34
|
+
(CONTENT_MODELS - [:bindings, :guide]).each do |model|
|
35
|
+
klass = self.class.const_get(model.to_s.capitalize)
|
36
|
+
obj = klass.new
|
37
|
+
__send__ "#{model}=", obj
|
38
|
+
end
|
39
|
+
yield self if block_given?
|
40
|
+
self
|
41
|
+
end
|
42
|
+
|
43
|
+
def make_metadata
|
44
|
+
self.metadata = Metadata.new
|
45
|
+
metadata.make do
|
46
|
+
yield metadata if block_given?
|
47
|
+
end
|
48
|
+
metadata
|
49
|
+
end
|
50
|
+
|
51
|
+
def make_manifest
|
52
|
+
self.manifest = Manifest.new
|
53
|
+
manifest.make do
|
54
|
+
yield manifest if block_given?
|
55
|
+
end
|
56
|
+
manifest
|
57
|
+
end
|
58
|
+
|
59
|
+
def make_spine
|
60
|
+
self.spine = Spine.new
|
61
|
+
spine.make do
|
62
|
+
yield spine if block_given?
|
63
|
+
end
|
64
|
+
spine
|
65
|
+
end
|
66
|
+
|
67
|
+
def make_bindings
|
68
|
+
self.bindings = Bindings.new
|
69
|
+
bindings.make do
|
70
|
+
yield bindings if block_given?
|
71
|
+
end
|
72
|
+
bindings
|
73
|
+
end
|
74
|
+
|
75
|
+
def save(archive)
|
76
|
+
archive.add_buffer book.rootfile_path, to_xml
|
77
|
+
end
|
78
|
+
|
79
|
+
module ContentModel
|
80
|
+
# @param [Nokogiri::XML::Builder::NodeBuilder] node
|
81
|
+
# @param [Object] model
|
82
|
+
# @param [Array<Symbol|String>] attributes names of attribute.
|
83
|
+
def to_xml_attribute(node, model, attributes)
|
84
|
+
attributes.each do |attr|
|
85
|
+
val = model.__send__(attr)
|
86
|
+
node[attr.to_s.gsub('_', '-')] = val if val
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
class Metadata
|
92
|
+
include ContentModel
|
93
|
+
|
94
|
+
def make
|
95
|
+
yield self if block_given?
|
96
|
+
unless unique_identifier
|
97
|
+
if identifiers.empty?
|
98
|
+
identifier = DCMES.new
|
99
|
+
identifier.id = 'pub-id'
|
100
|
+
identifier.content = UUID.create.to_s
|
101
|
+
self.dc_identifiers << identifier
|
102
|
+
self.unique_identifier = identifier
|
103
|
+
else
|
104
|
+
self.unique_identifier = identifiers.first
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
unless metas.any? {|meta| meta.property == 'dcterms:modified'}
|
109
|
+
modified = Meta.new
|
110
|
+
modified.property = 'dcterms:modified'
|
111
|
+
# modified.content = Time.now.utc.strftime('%FT%TZ')
|
112
|
+
modified.content = Time.now.utc.iso8601
|
113
|
+
self.metas << modified
|
114
|
+
end
|
115
|
+
|
116
|
+
self
|
117
|
+
end
|
118
|
+
|
119
|
+
def to_xml_fragment(xml)
|
120
|
+
xml.metadata_('xmlns:dc' => EPUB::NAMESPACES['dc']) {
|
121
|
+
(DC_ELEMS - [:languages]).each do |elems|
|
122
|
+
singular = elems[0..-2] + '_'
|
123
|
+
__send__("dc_#{elems}").each do |elem|
|
124
|
+
node = xml['dc'].__send__(singular, elem.content)
|
125
|
+
to_xml_attribute node, elem, [:id, :dir]
|
126
|
+
node['xml:lang'] = elem.lang if elem.lang
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
languages.each do |language|
|
131
|
+
xml['dc'].language language.content
|
132
|
+
end
|
133
|
+
|
134
|
+
metas.each do |meta|
|
135
|
+
node = xml.meta(meta.content)
|
136
|
+
to_xml_attribute node, meta, [:property, :id, :scheme]
|
137
|
+
node['refines'] = "##{meta.refines.id}" if meta.refines
|
138
|
+
end
|
139
|
+
|
140
|
+
links.each do |link|
|
141
|
+
node = xml.link
|
142
|
+
to_xml_attribute node, link, [:href, :id, :media_type]
|
143
|
+
node['rel'] = link.rel.join(' ') if link.rel
|
144
|
+
node['refines'] = "##{link.refines.id}" if link.refines
|
145
|
+
end
|
146
|
+
}
|
147
|
+
end
|
148
|
+
|
149
|
+
# Shortcut to set title from String
|
150
|
+
# @param title [String]
|
151
|
+
def title=(title)
|
152
|
+
t = Title.new
|
153
|
+
t.content = title
|
154
|
+
self.dc_titles = [t]
|
155
|
+
title
|
156
|
+
end
|
157
|
+
|
158
|
+
# Shortcut to set language from String
|
159
|
+
# @param lang_code [String]
|
160
|
+
def language=(lang_code)
|
161
|
+
lang = DCMES.new
|
162
|
+
lang.content = lang_code
|
163
|
+
self.dc_languages = [lang]
|
164
|
+
lang_code
|
165
|
+
end
|
166
|
+
|
167
|
+
# Shortcut to set one creator from String
|
168
|
+
# @param name [String]
|
169
|
+
def creator=(name)
|
170
|
+
creator = DCMES.new
|
171
|
+
creator.content = name
|
172
|
+
self.dc_creators = [creator]
|
173
|
+
name
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
class Manifest
|
178
|
+
include ContentModel
|
179
|
+
|
180
|
+
def make
|
181
|
+
yield self if block_given?
|
182
|
+
|
183
|
+
# @todo more careful
|
184
|
+
unless items.any? &:nav?
|
185
|
+
nav = Item.new
|
186
|
+
nav.id = 'toc'
|
187
|
+
nav.href = Addressable::URI.parse('nav.xhtml')
|
188
|
+
nav.media_type = 'application/xhtml+xml'
|
189
|
+
nav.properties << 'nav'
|
190
|
+
|
191
|
+
nav_doc = ContentDocument::Navigation.new
|
192
|
+
nav_doc.item = nav
|
193
|
+
|
194
|
+
nav_nav = ContentDocument::Navigation::Navigation.new
|
195
|
+
nav_nav.type = ContentDocument::Navigation::Navigation::Type::TOC
|
196
|
+
nav_nav.items = items.select(&:xhtml?).map {|item; nav|
|
197
|
+
nav = ContentDocument::Navigation::Item.new
|
198
|
+
nav.item = item
|
199
|
+
nav.href = item.href
|
200
|
+
nav.text = File.basename(item.href.normalize.request_uri, '.*')
|
201
|
+
nav
|
202
|
+
}
|
203
|
+
nav_doc.navigations << nav_nav
|
204
|
+
|
205
|
+
nav.content = nav_doc.to_xml
|
206
|
+
|
207
|
+
self << nav
|
208
|
+
end
|
209
|
+
|
210
|
+
self
|
211
|
+
end
|
212
|
+
|
213
|
+
def make_item(options={})
|
214
|
+
item = Item.new
|
215
|
+
[:id, :href, :media_type, :properties, :media_overlay].each do |attr|
|
216
|
+
next unless options.key? attr
|
217
|
+
item.__send__ "#{attr}=", options[attr]
|
218
|
+
end
|
219
|
+
item.manifest = self
|
220
|
+
yield item if block_given?
|
221
|
+
self << item
|
222
|
+
item
|
223
|
+
end
|
224
|
+
|
225
|
+
def to_xml_fragment(xml)
|
226
|
+
node = xml.manifest_ {
|
227
|
+
items.each do |item|
|
228
|
+
item_node = xml.item_
|
229
|
+
to_xml_attribute item_node, item, [:id, :href, :media_type, :media_overlay]
|
230
|
+
item_node['properties'] = item.properties.join(' ') unless item.properties.empty?
|
231
|
+
item_node['fallback'] = item.fallback.id if item.fallback
|
232
|
+
end
|
233
|
+
}
|
234
|
+
to_xml_attribute node, self, [:id]
|
235
|
+
end
|
236
|
+
|
237
|
+
class Item
|
238
|
+
attr_accessor :content, :content_file
|
239
|
+
|
240
|
+
# @param archive [Zip::Archive|nil] archive to save content. If nil, open archive in this method
|
241
|
+
# @raise StandardError when no content nor content_file
|
242
|
+
def save(archive=nil)
|
243
|
+
if archive
|
244
|
+
if content
|
245
|
+
archive.add_or_replace_buffer entry_name, content
|
246
|
+
elsif content_file
|
247
|
+
archive.add_or_replace_file entry_name, content_file
|
248
|
+
else
|
249
|
+
raise 'no content nor content_file'
|
250
|
+
end
|
251
|
+
else
|
252
|
+
Zip::Archive.open manifest.package.book.epub_file do |archive|
|
253
|
+
save archive
|
254
|
+
end
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
# Save document into EPUB archive when block ended
|
259
|
+
def edit
|
260
|
+
yield if block_given?
|
261
|
+
save
|
262
|
+
end
|
263
|
+
|
264
|
+
# Save document into EPUB archive at end of block
|
265
|
+
# @yield [REXML::Document]
|
266
|
+
def edit_with_rexml
|
267
|
+
require 'rexml/document'
|
268
|
+
doc = REXML::Document.new(read)
|
269
|
+
yield doc if block_given?
|
270
|
+
self.content = doc.to_s
|
271
|
+
save
|
272
|
+
end
|
273
|
+
|
274
|
+
# Save document into EPUB archive at end of block
|
275
|
+
# @yield [Nokgiri::XML::Document]
|
276
|
+
def edit_with_nokogiri
|
277
|
+
doc = Nokogiri.XML(read)
|
278
|
+
yield doc if block_given?
|
279
|
+
self.content = doc.to_xml
|
280
|
+
save
|
281
|
+
end
|
282
|
+
end
|
283
|
+
end
|
284
|
+
|
285
|
+
class Spine
|
286
|
+
include ContentModel
|
287
|
+
|
288
|
+
def make
|
289
|
+
yield self if block_given?
|
290
|
+
self
|
291
|
+
end
|
292
|
+
|
293
|
+
def make_itemref
|
294
|
+
itemref = Itemref.new
|
295
|
+
self << itemref
|
296
|
+
yield itemref if block_given?
|
297
|
+
itemref
|
298
|
+
end
|
299
|
+
|
300
|
+
def to_xml_fragment(xml)
|
301
|
+
node = xml.spine_ {
|
302
|
+
itemrefs.each do |itemref|
|
303
|
+
itemref_node = xml.itemref
|
304
|
+
to_xml_attribute itemref_node, itemref, [:idref, :id]
|
305
|
+
itemref_node['linear'] = 'no' unless itemref.linear?
|
306
|
+
itemref_node['properties'] = itemref.properties.join(' ') unless itemref.properties.empty?
|
307
|
+
end
|
308
|
+
}
|
309
|
+
to_xml_attribute node, self, [:id, :toc, :page_progression_direction]
|
310
|
+
end
|
311
|
+
end
|
312
|
+
|
313
|
+
class Bindings
|
314
|
+
include ContentModel
|
315
|
+
|
316
|
+
def make
|
317
|
+
yield self if block_given?
|
318
|
+
self
|
319
|
+
end
|
320
|
+
|
321
|
+
def make_media_type
|
322
|
+
media_type = MediaType.new
|
323
|
+
self << media_type
|
324
|
+
yield media_type if block_given?
|
325
|
+
media_type
|
326
|
+
end
|
327
|
+
|
328
|
+
def to_xml_fragment(xml)
|
329
|
+
xml.bindings_ {
|
330
|
+
media_types.each do |media_type|
|
331
|
+
media_type_node = xml.mediaType
|
332
|
+
to_xml_attribute media_type_node, media_type, [:media_type]
|
333
|
+
media_type_node['handler'] = media_type.handler.id if media_type.handler && media_type.handler.id
|
334
|
+
end
|
335
|
+
}
|
336
|
+
end
|
337
|
+
end
|
338
|
+
end
|
339
|
+
end
|
340
|
+
end
|
@@ -0,0 +1,134 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'rake/tasklib'
|
3
|
+
require 'epub/maker'
|
4
|
+
|
5
|
+
module EPUB
|
6
|
+
module Maker
|
7
|
+
class Task < ::Rake::TaskLib
|
8
|
+
attr_accessor :target, :base_dir, :files, :file_map_proc,
|
9
|
+
:container, :rootfiles, :make_rootfiles, :package_direction, :language,
|
10
|
+
:titles, :contributors,
|
11
|
+
:resources, :navs, :cover_image, :media_types,
|
12
|
+
:spine,
|
13
|
+
:bindings
|
14
|
+
|
15
|
+
# @param name [String] EPUB file name
|
16
|
+
def initialize(name)
|
17
|
+
init name
|
18
|
+
yield self if block_given?
|
19
|
+
define
|
20
|
+
end
|
21
|
+
|
22
|
+
def init(name)
|
23
|
+
@target = :epub
|
24
|
+
@name = name
|
25
|
+
@files = FileList.new
|
26
|
+
@base_dir = Dir.pwd
|
27
|
+
@rootfiles = FileList.new
|
28
|
+
@make_rootfiles = false
|
29
|
+
@package_direction = 'rtl'
|
30
|
+
@language = 'en'
|
31
|
+
@file_map = {}
|
32
|
+
@file_map_proc = -> (src_name) {src_name.sub("#{@base_dir.sub(/\/\z/, '')}/", '')}
|
33
|
+
@navs = FileList.new
|
34
|
+
@media_types = {}
|
35
|
+
@spine = FileList.new
|
36
|
+
@bindings = {}
|
37
|
+
end
|
38
|
+
|
39
|
+
def define
|
40
|
+
desc 'Make EPUB file'
|
41
|
+
task @target do
|
42
|
+
EPUB::Maker.make @name do |book|
|
43
|
+
book.make_ocf do |ocf|
|
44
|
+
if container
|
45
|
+
ocf.container = EPUB::Parser::OCF.new(nil).parse_container(File.read(container))
|
46
|
+
else
|
47
|
+
raise 'Set at least one rootfile' if rootfiles.empty?
|
48
|
+
ocf.make_container do |container|
|
49
|
+
rootfiles.each do |rootfile|
|
50
|
+
container.make_rootfile full_path: Addressable::URI.parse(file_map[rootfile])
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
if make_rootfiles
|
57
|
+
book.make_package do |package|
|
58
|
+
package.dir = package_direction
|
59
|
+
|
60
|
+
package.make_metadata do |metadata|
|
61
|
+
metadata.title = @titles.first
|
62
|
+
metadata.language = language
|
63
|
+
end
|
64
|
+
|
65
|
+
package.make_manifest do |manifest|
|
66
|
+
rootfile_path = Pathname(package.book.rootfile_path)
|
67
|
+
resources.each_with_index do |resource, index|
|
68
|
+
resource_path = Pathname(file_map[resource])
|
69
|
+
manifest.make_item do |item|
|
70
|
+
item.id = "item-#{index + 1}"
|
71
|
+
href = resource_path.relative_path_from(rootfile_path.parent)
|
72
|
+
item.href = Addressable::URI.parse(href.to_path)
|
73
|
+
item.media_type = media_types[resource] ||
|
74
|
+
case resource_path.extname
|
75
|
+
when '.xhtml', '.html' then 'application/xhtml+xml'
|
76
|
+
end
|
77
|
+
item.content_file = resource
|
78
|
+
item.properties << 'nav' if navs.include? item.entry_name
|
79
|
+
item.properties << 'scripted' unless Nokogiri.XML(open(resource)).search('script').empty?
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
package.make_spine do |spine|
|
85
|
+
@spine.each do |item_path|
|
86
|
+
entry_name = file_map[item_path]
|
87
|
+
spine.make_itemref do |itemref|
|
88
|
+
item = package.manifest.items.find {|i| i.entry_name == entry_name}
|
89
|
+
itemref.item = item if item
|
90
|
+
warn "missing item #{item_path}, referred by itemref" if itemref.item.nil?
|
91
|
+
itemref.linear = true # TODO: Make more customizable
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
if @bindings and !@bindings.empty?
|
97
|
+
package.make_bindings do |bindings|
|
98
|
+
@bindings.each_pair do |media_type, handler_path|
|
99
|
+
bindings.make_media_type do |mt|
|
100
|
+
mt.media_type = media_type
|
101
|
+
entry_name = file_map[handler_path]
|
102
|
+
mt.handler = package.manifest.items.find {|item| item.entry_name == entry_name}
|
103
|
+
warn "missing handler for #{media_type}" if mt.handler.nil?
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
else
|
110
|
+
raise 'No rootfile set' if @rootfiles.empty?
|
111
|
+
rf = rootfiles.first
|
112
|
+
book.package = EPUB::Parser::Publication.new(File.read(rf), file_map[rf]).parse
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def rootfile=(rootfile)
|
119
|
+
rootfiles.unshift rootfile
|
120
|
+
rootfile
|
121
|
+
end
|
122
|
+
|
123
|
+
def file_map(force_calculation=false)
|
124
|
+
if force_calculation or @file_map.empty?
|
125
|
+
@file_map.clear
|
126
|
+
@files.each do |src_name|
|
127
|
+
@file_map[src_name] = @file_map_proc[src_name]
|
128
|
+
end
|
129
|
+
end
|
130
|
+
@file_map
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|