epub-maker 0.0.1

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.
@@ -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