epub-maker 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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